Ưu điểm / nhược điểm của việc có tất cả các biến được khai báo trong bài kiểm tra JUnit


9

Tôi đã viết một số bài kiểm tra đơn vị cho một số mã mới tại nơi làm việc và đã gửi nó đi để xem xét mã. Một trong những đồng nghiệp của tôi đã đưa ra nhận xét về lý do tại sao tôi đặt các biến được sử dụng trong một số thử nghiệm ngoài phạm vi cho thử nghiệm.

Mã tôi đã đăng về cơ bản là

import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        new Foo(VALID_NAME);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        new Foo(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        new Foo("");
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " " + VALID_NAME + " ";
        final Foo foo = new Foo(name);

        assertThat(foo.getName(), is(equalTo(VALID_NAME)));
    }

    private static final String VALID_NAME = "name";
}

Những thay đổi được đề xuất của ông về cơ bản là

import org.junit.Test;

public class FooUnitTests {

    @Test
    public void testConstructorWithValidName() {
        final String name = "name";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithNullName() {
        final String name = null;
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithZeroLengthName() {
        final String name = "";
        final Foo foo = new Foo(name);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testConstructorWithLeadingAndTrailingWhitespaceInName() {
        final String name = " name ";
        final Foo foo = new Foo(name);

        final String actual = foo.getName();
        final String expected = "name";

        assertThat(actual, is(equalTo(expected)));
    }
}

Trong đó mọi thứ được yêu cầu trong phạm vi thử nghiệm, được xác định trong phạm vi thử nghiệm.

Một số lợi thế mà ông lập luận là

  1. Mỗi bài kiểm tra là khép kín.
  2. Mỗi thử nghiệm có thể được thực hiện trong sự cô lập, hoặc tổng hợp, với cùng một kết quả.
  3. Người kiểm tra không phải cuộn đến bất cứ nơi nào các tham số này được khai báo để tra cứu giá trị là gì.

Một số nhược điểm của phương pháp mà tôi lập luận là

  1. Tăng sao chép mã
  2. Có thể thêm nhiễu vào tâm trí người đánh giá nếu có một số thử nghiệm tương tự với các giá trị khác nhau được xác định (nghĩa là thử nghiệm với doFoo(bar)kết quả trong một giá trị, trong khi cùng một cuộc gọi có kết quả khác nhau vì barđược định nghĩa khác nhau trong phương thức đó).

Ngoài quy ước, có bất kỳ lợi thế / bất lợi nào khác của việc sử dụng phương pháp này so với phương pháp khác không?


Vấn đề duy nhất tôi thấy với mã của bạn là bạn đã chôn phần khai báo của VALID_NAME ở phía dưới khiến chúng tôi tự hỏi nó đến từ đâu. Sửa nó sẽ chăm sóc "cuộn". Tên phạm vi địa phương đồng nghiệp của bạn đang vi phạm DRY mà không có lý do. Hỏi anh ta tại sao nên sử dụng các câu lệnh nhập bên ngoài mỗi phương thức kiểm tra. Sheesh nó được gọi là bối cảnh.
candied_orange

@CandiedOrange: không có ai ở đây đọc câu trả lời của tôi? Cách tiếp cận đồng nghiệp có thể vi phạm nguyên tắc DRY, nhưng không phải trong ví dụ này. Không có điểm nào có cùng một "tên" văn bản trong tất cả các trường hợp này.
Doc Brown

Các biến bổ sung không thêm rõ ràng, tuy nhiên, điều đó chỉ ra rằng bạn có thể viết lại các bài kiểm tra của mình để sử dụng Lý thuyết và Dữ liệu của Junit, điều này sẽ làm giảm các bài kiểm tra của bạn rất nhiều!
Rosa Richter

1
@CandiedOrange: DRY ít liên quan đến việc tránh sao chép mã hơn là tránh trùng lặp kiến ​​thức (sự kiện, quy tắc, v.v.). Xem: hermanradtke.com/2013/02/06/misunder Hiểu-dry.html Nguyên tắc Dry được nêu là "Mỗi phần kiến ​​thức phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống." vi.wikipedia.org/wiki/Don%27t numpeat_yourself
Marjan Venema

Hầu hết mọi kiến ​​thức đều phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống. Vậy thực tế "tên" là một VALID_NAME KHÔNG phải là một trong những kiến ​​thức này? Đúng là DRY không chỉ tránh việc sao chép mã mà bạn sẽ thấy khó có thể DRY với mã trùng lặp.
candied_orange

Câu trả lời:


10

Bạn nên tiếp tục làm những gì bạn đang làm.

Lặp đi lặp lại chính mình cũng là một ý tưởng tồi trong mã kiểm tra như trong mã doanh nghiệp và vì lý do tương tự. Đồng nghiệp của bạn đã bị đánh lừa bởi ý tưởng rằng mọi bài kiểm tra nên được khép kín . Điều đó khá đúng, nhưng "khép kín" không có nghĩa là nó nên chứa mọi thứ nó cần trong cơ thể phương thức của nó. Điều đó chỉ có nghĩa là nó sẽ cho kết quả tương tự cho dù nó được thực hiện trong cô lập hay là một phần của bộ và bất kể thứ tự của các thử nghiệm trong bộ. Nói cách khác, mã kiểm tra phải có cùng một ngữ nghĩa bất kể mã nào khác đã thực thi trước nó ; nó không phải có tất cả các mã yêu cầu được gói theo văn bản .

Việc sử dụng lại các hằng số, mã thiết lập và tương tự làm tăng chất lượng của mã kiểm tra và không ảnh hưởng đến tính độc lập của nó, vì vậy đó là điều tốt mà bạn nên tiếp tục làm.


+1 hoàn toàn, nhưng xin lưu ý rằng DRY ít tránh việc lặp lại mã hơn là không lặp lại kiến ​​thức. Xem hermanradtke.com/2013/02/06/misunder Hiểu-hard.html . Nguyên tắc Dry được nêu là "Mỗi phần kiến ​​thức phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống." vi.wikipedia.org/wiki/Don%27t numpeat_yourself
Marjan Venema

3

Nó phụ thuộc. Nói chung, có các hằng số lặp lại được tuyên bố ở một nơi duy nhất là một điều tốt.

Tuy nhiên, trong ví dụ của bạn, không có điểm nào trong việc xác định thành viên VALID_NAMEtheo cách hiển thị. Giả sử trong biến thể thứ hai, một người bảo trì thay đổi văn bản nametrong thử nghiệm đầu tiên, thử nghiệm thứ hai có thể sẽ vẫn còn hiệu lực và ngược lại. Ví dụ: giả sử bạn muốn kiểm tra thêm chữ hoa và chữ thường, nhưng cũng giữ một trường hợp kiểm tra chỉ với chữ in thường, với càng ít trường hợp kiểm tra càng tốt. Thay vì thêm một thử nghiệm mới, bạn có thể thay đổi thử nghiệm đầu tiên thành

public void testConstructorWithValidName() {
    final String name = "Name";
    final Foo foo = new Foo(name);
}

và vẫn giữ các bài kiểm tra còn lại.

Trong thực tế, có rất nhiều bài kiểm tra tùy thuộc vào một biến như vậy và việc thay đổi giá trị VALID_NAMEsau này có thể vô tình phá vỡ một số bài kiểm tra của bạn tại một thời điểm sau đó. Và trong tình huống như vậy, bạn thực sự có thể cải thiện khả năng tự kiểm tra của mình bằng cách không đưa ra hằng số nhân tạo với các thử nghiệm khác nhau tùy thuộc vào nó.

Tuy nhiên, những gì @KilianFoth đã viết về việc không lặp lại chính mình cũng là chính xác: tuân theo nguyên tắc DRY trong mã kiểm tra giống như cách bạn thực hiện trong mã doanh nghiệp. Ví dụ: giới thiệu các hằng hoặc biến thành viên khi cần thiết cho mã kiểm tra của bạn rằng giá trị của chúng là giống nhau ở mọi nơi.

Ví dụ của bạn cho thấy các thử nghiệm có xu hướng lặp lại mã khởi tạo như thế nào, đó là một nơi điển hình để bắt đầu với tái cấu trúc. Vì vậy, bạn có thể xem xét để cấu trúc lại sự lặp lại của

  new Foo(...);

vào một chức năng riêng biệt

 public Foo ConstructFoo(String name) 
 {
     return new Foo(name);
 }

vì nó sẽ cho phép bạn thay đổi chữ ký của nhà Fooxây dựng tại một thời điểm sau đó dễ dàng hơn (vì có ít nơi new Foođược gọi là thực sự hơn).


Đây chỉ là một mã ví dụ rất ngắn. Trong mã sản xuất, biến VALID_NAME được sử dụng ~ 30 lần. Tuy nhiên, đó là một điểm rất thực phẩm và một điểm mà tôi không có.
Zymus

@Zymus: số lần VALID_NAME được sử dụng là IMHO không liên quan - điều quan trọng là nếu các bài kiểm tra của bạn sử dụng cùng một văn bản "tên" trong mọi trường hợp (vì vậy sẽ rất hợp lý khi giới thiệu một biểu tượng cho nó) hoặc nếu giới thiệu biểu tượng tạo ra một sự phụ thuộc nhân tạo, không cần thiết. Tôi chỉ có thể xem ví dụ của bạn là IMHO thuộc loại thứ hai, nhưng đối với mã "thực" của bạn, số dặm của bạn có thể thay đổi.
Doc Brown

1
+1 cho trường hợp bạn thực hiện. Và mã kiểm tra nên là mã cấp sản xuất. Điều đó nói rằng, DRY không phải là quá nhiều về việc không lặp lại chính mình. Đó là về việc không lặp lại kiến ​​thức. Nguyên tắc DRY được tuyên bố là Mười Mỗi phần kiến ​​thức phải có một đại diện duy nhất, rõ ràng, có thẩm quyền trong một hệ thống. ( vi.wikipedia.org/wiki/Don%27t numpeat_yourself ). Những hiểu lầm nảy sinh từ "lặp lại" Tôi nghĩ, cũng như: hermanradtke.com/2013/02/06/misunder Hiểu
Marjan Venema

1

Tôi thực sự sẽ ... không đi cho cả hai , nhưng tôi sẽ chọn bạn qua đồng nghiệp của bạn để lấy ví dụ đơn giản hóa của bạn.

Trước tiên, tôi có xu hướng ủng hộ các giá trị nội tuyến thay vì thực hiện các bài tập một lần. Điều này giúp cải thiện khả năng đọc mã cho tôi vì tôi không phải chia nhỏ suy nghĩ của mình thành nhiều câu lệnh gán, sau đó đọc (các) dòng mã nơi chúng được sử dụng. Do đó, tôi sẽ thích cách tiếp cận của bạn hơn so với đồng nghiệp của bạn, với các cú đánh nhẹ testConstructorWithLeadingAndTrailingWhitespaceInName()có thể chỉ là:

assertThat(new Foo(" " + VALID_NAME + " ").getName(), is(equalTo(VALID_NAME)));

Điều đó mang lại cho tôi để đặt tên. Thông thường, nếu bài kiểm tra của bạn khẳng định Exceptionsẽ bị ném, tên bài kiểm tra của bạn cũng nên mô tả hành vi đó cho rõ ràng. Sử dụng ví dụ trên, vậy nếu tôi có thêm khoảng trắng trong tên khi tôi xây dựng Foođối tượng thì sao? Và làm thế nào là liên quan đến ném một IllegalArgumentException?

Dựa trên null""các bài kiểm tra, tôi đoán ở đây rằng điều tương tự Exceptionđược ném lần này để gọi getName(). Nếu thực sự đúng như vậy, có thể đặt tên của phương thức thử là callingGetNameWithWhitespacePaddingThrows()( IllegalArgumentException- mà tôi nghĩ sẽ là một phần của đầu ra của JUnit, do đó tùy chọn) sẽ tốt hơn. Nếu có phần đệm khoảng trắng trong tên trong khi khởi tạo cũng sẽ ném như vậy Exception, thì tôi cũng sẽ bỏ qua hoàn toàn xác nhận, để làm rõ rằng đó là hành vi của nhà xây dựng.

Tuy nhiên, tôi nghĩ rằng có một cách tốt hơn để làm mọi thứ ở đây khi bạn nhận được nhiều hoán vị hơn cho các đầu vào hợp lệ và không hợp lệ, cụ thể là kiểm tra tham số . Những gì bạn đang kiểm tra chính xác là: Bạn đang tạo ra một loạt các Foođối tượng với các đối số hàm tạo khác nhau, và sau đó khẳng định nó không thành công khi khởi tạo hoặc gọi getName(). Vì vậy, tại sao không để JUnit thực hiện việc lặp lại và giàn giáo cho bạn? Về mặt giáo dân, bạn có thể nói với JUnit, "đây là những giá trị tôi muốn tạo một Foođối tượng", và sau đó phương thức thử nghiệm của bạn khẳng địnhIllegalArgumentExceptionở đúng nơi Thật không may, vì cách kiểm tra tham số của JUnit không hoạt động tốt với kiểm tra cho cả các trường hợp thành công và ngoại lệ trong cùng một bài kiểm tra (theo như tôi biết), có lẽ bạn sẽ cần phải vượt qua một chút cấu trúc sau:

@RunWith(Parameterized.class)
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @Parameters
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] {
                 { VALID_NAME, false }, 
                 { "", true }, 
                 { null, true }, 
                 { " " + VALID_NAME + " ", true }
           });
    }

    private String input;

    private boolean throwException;

    public FooUnitTests(String input, boolean throwException) {
        this.input = input;
        this.throwException = throwException;
    }

    @Test
    public void test() {
        try {
            Foo test = new Foo(input);
            // TODO any testing required for VALID_NAME?
            if (throwException) {
                assertEquals(VALID_NAME, test.getName());
            }
        } catch (IllegalArgumentException e) {
            if (!throwException) {
                throw new RuntimeException(e);
            }
        }
    }
}

Phải thừa nhận rằng, điều này có vẻ khó hiểu đối với thử nghiệm bốn giá trị đơn giản khác và nó không giúp ích gì nhiều cho việc triển khai có phần hạn chế của JUnit. Về vấn đề này, tôi thấy cách tiếp cận của TestNG@DataProvider hữu ích hơn và chắc chắn nó đáng để xem xét kỹ hơn nếu bạn đang xem xét các thử nghiệm được tham số hóa.

Đây là cách người ta có thể sử dụng TestNG cho các thử nghiệm đơn vị của bạn (sử dụng Java 8 Streamđể đơn giản hóa việc Iteratorxây dựng). Một số lợi ích là, nhưng không giới hạn ở:

  1. Các tham số kiểm tra trở thành đối số phương thức, không phải các trường lớp.
  2. Bạn có thể có nhiều @DataProviders cung cấp các loại đầu vào khác nhau.
    • Có thể rõ ràng hơn và dễ dàng hơn để thêm các đầu vào hợp lệ / không hợp lệ mới để thử nghiệm.
  3. Nó vẫn có vẻ hơi quá so với ví dụ đơn giản hóa của bạn, nhưng IMHO nó gọn gàng hơn phương pháp của JUnit.
public class FooUnitTests {

    private static final String VALID_NAME = "ABC";

    @DataProvider(name="successes")
    public static Iterator<Object[]> getSuccessCases() {
        return Stream.of(VALID_NAME).map(v -> new Object[]{ v }).iterator();
    }

    @DataProvider(name="exceptions")
    public static Iterator<Object[]> getExceptionCases() {
        return Stream.of("", null, " " + VALID_NAME + " ")
                    .map(v -> new Object[]{ v }).iterator();
    }

    @Test(dataProvider="successes")
    public void testSuccesses(String input) {
        new Foo(input);
        // TODO any further testing required?
    }

    @Test(dataProvider="exceptions", expectedExceptions=IllegalArgumentException.class)
    public void testExceptions(String input) {
        Foo test = new Foo(input);
        if (input.contains(VALID_NAME)) {
            // TestNG favors assert(actual, expected).
            assertEquals(test.getName(), VALID_NAME);
        }
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.