วิธีทดสอบคลาสนามธรรมใน Java ด้วย JUnit


87

ฉันยังใหม่กับการทดสอบ Java ด้วย JUnit ฉันต้องทำงานกับ Java และฉันต้องการใช้การทดสอบหน่วย

ปัญหาของฉันคือฉันมีคลาสนามธรรมที่มีวิธีนามธรรม แต่มีบางวิธีที่ไม่เป็นนามธรรม ฉันจะทดสอบคลาสนี้ด้วย JUnit ได้อย่างไร? ตัวอย่างรหัส (ง่ายมาก):

abstract class Car {

    public Car(int speed, int fuel) {
        this.speed = speed;
        this.fuel = fuel;
    }

    private int speed;
    private int fuel;

    abstract void drive();

    public int getSpeed() {
        return this.speed;
    }

    public int getFuel() {
        return this.fuel;
    }
}

ฉันต้องการทดสอบgetSpeed()และgetFuel()ฟังก์ชั่น

คำถามที่คล้ายกันสำหรับปัญหานี้อยู่ที่นี่แต่ไม่ได้ใช้ JUnit

ในส่วนคำถามที่พบบ่อยของ JUnit ฉันพบลิงค์นี้แต่ฉันไม่เข้าใจว่าผู้เขียนต้องการพูดอะไรกับตัวอย่างนี้ บรรทัดของรหัสนี้หมายถึงอะไร?

public abstract Source getSource() ;

4
ดูstackoverflow.com/questions/1087339/…สำหรับสองโซลูชันโดยใช้ Mockito
ddso

มีข้อได้เปรียบในการเรียนรู้กรอบงานอื่นสำหรับการทดสอบหรือไม่? Mockito เป็นเพียงส่วนขยายของ jUnit หรือโปรเจ็กต์ที่แตกต่างอย่างสิ้นเชิง?
vasco

Mockito ไม่ได้แทนที่ JUnit เช่นเดียวกับกรอบการเยาะเย้ยอื่น ๆ มันถูกใช้นอกเหนือจากกรอบการทดสอบหน่วยและช่วยคุณในการสร้างวัตถุจำลองเพื่อใช้ในกรณีทดสอบของคุณ
ddso

1
ไม่เชื่อเรื่องพระเจ้าภาษา: stackoverflow.com/questions/243274/…
Ciro Santilli 郝海东冠状病六四事件法轮功

คำตอบ:


105

หากคุณไม่มีการนำคลาสไปใช้อย่างเป็นรูปธรรมและวิธีการต่างๆไม่ได้staticเป็นจุดทดสอบอะไร? หากคุณมีคลาสคอนกรีตคุณจะทดสอบวิธีการเหล่านั้นเป็นส่วนหนึ่งของ API สาธารณะของคลาสคอนกรีต

ฉันรู้ว่าคุณกำลังคิดอะไร "ฉันไม่ต้องการทดสอบวิธีการเหล่านี้ซ้ำแล้วซ้ำเล่านั่นคือเหตุผลที่ฉันสร้างคลาสนามธรรม" แต่ข้อโต้แย้งที่โต้แย้งของฉันคือประเด็นของการทดสอบหน่วยคือการอนุญาตให้นักพัฒนาทำการเปลี่ยนแปลง เรียกใช้การทดสอบและวิเคราะห์ผลลัพธ์ ส่วนหนึ่งของการเปลี่ยนแปลงเหล่านั้นอาจรวมถึงการลบล้างวิธีการของคลาสนามธรรมของคุณทั้งprotectedและpublicซึ่งอาจส่งผลให้เกิดการเปลี่ยนแปลงพฤติกรรมขั้นพื้นฐาน ขึ้นอยู่กับลักษณะของการเปลี่ยนแปลงเหล่านั้นซึ่งอาจส่งผลต่อการทำงานของแอปพลิเคชันของคุณในรูปแบบที่ไม่คาดคิดและอาจเป็นไปในทางลบ หากคุณมีปัญหาเกี่ยวกับชุดทดสอบหน่วยที่ดีที่เกิดจากการเปลี่ยนแปลงประเภทเหล่านี้ควรปรากฏให้เห็นในขณะพัฒนา


17
ความครอบคลุมของรหัส 100% เป็นตำนาน คุณควรมีการทดสอบเพียงพอที่จะครอบคลุมสมมติฐานที่ทราบทั้งหมดเกี่ยวกับการทำงานของแอปพลิเคชันของคุณ (ควรเขียนก่อนที่คุณจะเขียนโค้ด la Test Driven Development) ขณะนี้ฉันกำลังทำงานกับทีม TDD ที่ทำงานได้ดีมากและเรามีความครอบคลุมเพียง 63% จากงานสร้างล่าสุดของเราทั้งหมดที่เขียนในขณะที่เราพัฒนา ที่ดีหรือไม่? ใครจะไปรู้ แต่ฉันคิดว่ามันเสียเวลาที่จะย้อนกลับไปและพยายามเพิ่มให้สูงขึ้น
nsfyn55

3
แน่นอน. บางคนอาจโต้แย้งว่าเป็นการละเมิด TDD ที่ดี ลองนึกภาพคุณอยู่ในทีม คุณตั้งสมมติฐานว่าวิธีนี้ถือเป็นที่สิ้นสุดและไม่ต้องทำการทดสอบในการนำไปใช้อย่างเป็นรูปธรรมใด ๆ มีคนลบตัวปรับแต่งและทำการเปลี่ยนแปลงที่กระเพื่อมทั้งสาขาของลำดับชั้นการสืบทอด คุณไม่ต้องการให้ชุดทดสอบของคุณจับได้หรือไม่?
nsfyn55

31
ฉันไม่เห็นด้วย. ไม่ว่าคุณจะทำงานใน TDD หรือไม่ก็ตามวิธีการที่เป็นรูปธรรมของคลาสนามธรรมของคุณมีโค้ดดังนั้นพวกเขาควรมีการทดสอบ (ไม่ว่าจะมีคลาสย่อยหรือไม่ก็ตาม) ยิ่งไปกว่านั้นการทดสอบหน่วยในการทดสอบ Java (ปกติ) ดังนั้นจึงไม่มีตรรกะใด ๆ ในวิธีการทดสอบที่ไม่ได้เป็นส่วนหนึ่งของคลาส แต่เป็นของคลาสระดับสูง ตามตรรกะนั้นเราไม่ควรทดสอบคลาสใด ๆ ใน Java ยกเว้นคลาสที่ไม่มีคลาสย่อยเลย เกี่ยวกับวิธีการที่ถูกแทนที่นั่นคือเมื่อคุณเพิ่มการทดสอบเพื่อตรวจสอบการเปลี่ยนแปลง / เพิ่มเติมในการทดสอบของคลาสย่อย
ethanfar

3
@ nsfyn55 ถ้าคอนกรีตเป็นfinalอย่างไร? ฉันไม่เห็นเหตุผลในการทดสอบวิธีการเดียวกันหลาย ๆ ครั้งหากการใช้งานไม่สามารถเปลี่ยนแปลงได้
ไดออกซิน

3
เราไม่ควรให้การทดสอบกำหนดเป้าหมายไปที่อินเทอร์เฟซนามธรรมเพื่อให้เราสามารถเรียกใช้สำหรับการใช้งานทั้งหมดหรือไม่? หากเป็นไปไม่ได้แสดงว่าเรากำลังละเมิดลิสคอฟซึ่งเราต้องการทราบและแก้ไข เฉพาะในกรณีที่การใช้งานเพิ่มฟังก์ชันเพิ่มเติม (เข้ากันได้) บางอย่างเราควรมีการทดสอบหน่วยเฉพาะสำหรับมัน (และสำหรับฟังก์ชันพิเศษนั้นเท่านั้น)
tne

36

สร้างคลาสคอนกรีตที่สืบทอดคลาสนามธรรมจากนั้นทดสอบฟังก์ชันที่คลาสคอนกรีตสืบทอดมาจากคลาสนามธรรม


คุณจะทำอย่างไรในกรณีที่คุณมีคลาสที่เป็นรูปธรรม 10 คลาสที่ขยายคลาสนามธรรมและแต่ละคลาสที่เป็นรูปธรรมเหล่านี้จะใช้เพียง 1 วิธีและสมมติว่าอีก 2 วิธีนั้นเหมือนกันสำหรับแต่ละคลาสเหล่านี้เนื่องจากมีการนำไปใช้ในบทคัดย่อ ชั้น? กรณีของฉันคือฉันไม่ต้องการคัดลอกวางการทดสอบสำหรับคลาสนามธรรมในแต่ละคลาสย่อย
scarface

12

ด้วยคลาสตัวอย่างที่คุณโพสต์ดูเหมือนจะไม่สมเหตุสมผลที่จะทดสอบgetFuel()และgetSpeed()เนื่องจากพวกเขาสามารถคืนค่า 0 เท่านั้น (ไม่มีตัวตั้งค่า)

อย่างไรก็ตามหากสมมติว่านี่เป็นเพียงตัวอย่างที่เรียบง่ายเพื่อจุดประสงค์ในการอธิบายและคุณมีเหตุผลที่ถูกต้องในการทดสอบวิธีการในคลาสฐานนามธรรม (คนอื่น ๆ ได้ชี้ให้เห็นถึงผลกระทบแล้ว) คุณสามารถตั้งค่ารหัสทดสอบของคุณเพื่อสร้างแบบไม่ระบุตัวตนได้ คลาสย่อยของคลาสพื้นฐานที่จัดเตรียมการใช้งานดัมมี่ (no-op) สำหรับวิธีนามธรรม

ตัวอย่างเช่นTestCaseคุณสามารถทำได้:

c = new Car() {
       void drive() { };
   };

จากนั้นทดสอบวิธีที่เหลือเช่น:

public class CarTest extends TestCase
{
    private Car c;

    public void setUp()
    {
        c = new Car() {
            void drive() { };
        };
    }

    public void testGetFuel() 
    {
        assertEquals(c.getFuel(), 0);
    }

    [...]
}

(ตัวอย่างนี้อ้างอิงจากไวยากรณ์ JUnit3 สำหรับ JUnit4 โค้ดจะแตกต่างกันเล็กน้อย แต่แนวคิดเหมือนกัน)


ขอบคุณสำหรับคำตอบ. ใช่ตัวอย่างของฉันง่ายขึ้น (และไม่ค่อยดีนัก) หลังจากอ่านคำตอบทั้งหมดที่นี่ฉันเขียนคลาสดัมมี่ แต่ตามที่เขียน @ nsfyn55 ในคำตอบของเขาฉันเขียนแบบทดสอบสำหรับทายาททุกคนของคลาสนามธรรมนี้
vasco

9

หากคุณต้องการวิธีแก้ปัญหา (เช่นเนื่องจากคุณมีการใช้งานคลาสนามธรรมมากเกินไปและการทดสอบมักจะทำซ้ำขั้นตอนเดียวกัน) คุณสามารถสร้างคลาสทดสอบนามธรรมด้วยวิธีการโรงงานนามธรรมซึ่งจะแยกออกจากการใช้งานนั้น ชั้นทดสอบ ตัวอย่างนี้ใช้ได้ผลหรือฉันกับ TestNG:

คลาสทดสอบนามธรรมของCar:

abstract class CarTest {

// the factory method
abstract Car createCar(int speed, int fuel);

// all test methods need to make use of the factory method to create the instance of a car
@Test
public void testGetSpeed() {
    Car car = createCar(33, 44);
    assertEquals(car.getSpeed(), 33);
    ...

การนำไปใช้งาน Car

class ElectricCar extends Car {

    private final int batteryCapacity;

    public ElectricCar(int speed, int fuel, int batteryCapacity) {
        super(speed, fuel);
        this.batteryCapacity = batteryCapacity;
    }

    ...

ชั้นทดสอบหน่วยElectricCarTestของชั้นเรียนElectricCar:

class ElectricCarTest extends CarTest {

    // implementation of the abstract factory method
    Car createCar(int speed, int fuel) {
        return new ElectricCar(speed, fuel, 0);
    }

    // here you cann add specific test methods
    ...

5

คุณสามารถทำสิ่งนี้ได้

public abstract MyAbstractClass {

    @Autowire
    private MyMock myMock;        

    protected String sayHello() {
            return myMock.getHello() + ", " + getName();
    }

    public abstract String getName();
}

// this is your JUnit test
public class MyAbstractClassTest extends MyAbstractClass {

    @Mock
    private MyMock myMock;

    @InjectMocks
    private MyAbstractClass thiz = this;

    private String myName = null;

    @Override
    public String getName() {
        return myName;
    }

    @Test
    public void testSayHello() {
        myName = "Johnny"
        when(myMock.getHello()).thenReturn("Hello");
        String result = sayHello();
        assertEquals("Hello, Johnny", result);
    }
}

4

ฉันจะสร้างคลาสภายใน jUnit ที่สืบทอดมาจากคลาสนามธรรม สิ่งนี้สามารถสร้างอินสแตนซ์และเข้าถึงวิธีการทั้งหมดที่กำหนดไว้ในคลาสนามธรรม

public class AbstractClassTest {
   public void testMethod() {
   ...
   }
}


class ConcreteClass extends AbstractClass {

}

3
นี่คือคำแนะนำที่ดีเยี่ยม สามารถปรับปรุงได้โดยการให้ตัวอย่างแม้ว่า บางทีอาจเป็นตัวอย่างของชั้นเรียนที่คุณกำลังอธิบาย
SDJMcHattie

2

คุณสามารถสร้างอินสแตนซ์คลาสที่ไม่ระบุชื่อจากนั้นทดสอบคลาสนั้น

public class ClassUnderTest_Test {

    private ClassUnderTest classUnderTest;

    private MyDependencyService myDependencyService;

    @Before
    public void setUp() throws Exception {
        this.myDependencyService = new MyDependencyService();
        this.classUnderTest = getInstance();
    }

    private ClassUnderTest getInstance() {
        return new ClassUnderTest() {    
            private ClassUnderTest init(
                    MyDependencyService myDependencyService
            ) {
                this.myDependencyService = myDependencyService;
                return this;
            }

            @Override
            protected void myMethodToTest() {
                return super.myMethodToTest();
            }
        }.init(myDependencyService);
    }
}

โปรดจำไว้ว่าการมองเห็นจะต้องprotectedสำหรับคุณสมบัติของระดับนามธรรมmyDependencyServiceClassUnderTest

คุณยังสามารถรวมแนวทางนี้เข้ากับ Mockito ได้อย่างลงตัว ดูที่นี่ .


2

วิธีการทดสอบของฉันค่อนข้างง่ายภายในแต่ละabstractUnitTest.javaข้อ ฉันสร้างคลาสใน abstractUnitTest.java ที่ขยายคลาสนามธรรม และทดสอบด้วยวิธีนั้น


0

คุณไม่สามารถทดสอบคลาสนามธรรมทั้งหมดได้ ในกรณีนี้คุณมีวิธีการที่เป็นนามธรรมนั่นหมายความว่าควรใช้วิธีนี้โดยคลาสที่ขยายคลาสนามธรรมที่กำหนด

ในโปรแกรมเมอร์ระดับนั้นจะต้องเขียนซอร์สโค้ดที่มีไว้สำหรับตรรกะของเขา

กล่าวอีกนัยหนึ่งคือไม่มีความรู้สึกในการทดสอบคลาสนามธรรมเนื่องจากคุณไม่สามารถตรวจสอบพฤติกรรมขั้นสุดท้ายของมันได้

หากคุณมีฟังก์ชันหลักที่ไม่เกี่ยวข้องกับวิธีการนามธรรมในคลาสนามธรรมบางอย่างให้สร้างคลาสอื่นที่วิธีนามธรรมจะทำให้เกิดข้อยกเว้น


0

คุณสามารถสร้างคลาสทดสอบนามธรรมที่ครอบคลุมตรรกะภายในคลาสนามธรรมและขยายสำหรับการทดสอบคลาสย่อยแต่ละคลาสได้ ด้วยวิธีนี้คุณสามารถมั่นใจได้ว่าตรรกะนี้จะได้รับการทดสอบสำหรับเด็กแต่ละคนแยกกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.