Thực tiễn tốt nhất: Khởi tạo các trường lớp JUnit trong setUp () hay tại khai báo?


120

Tôi có nên khởi tạo các trường lớp khi khai báo như thế này không?

public class SomeTest extends TestCase
{
    private final List list = new ArrayList();

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Hoặc trong setUp () như thế này?

public class SomeTest extends TestCase
{
    private List list;

    @Override
    protected void setUp() throws Exception
    {
        super.setUp();
        this.list = new ArrayList();
    }

    public void testPopulateList()
    {
        // Add stuff to the list
        // Assert the list contains what I expect
    }
}

Tôi có xu hướng sử dụng biểu mẫu đầu tiên vì nó ngắn gọn hơn và cho phép tôi sử dụng các trường cuối cùng. Nếu tôi không cần sử dụng phương thức setUp () để thiết lập, tôi vẫn nên sử dụng nó và tại sao?

Làm rõ: JUnit sẽ khởi tạo lớp thử nghiệm một lần cho mỗi phương pháp thử nghiệm. Điều đó có nghĩa là listsẽ được tạo một lần cho mỗi lần kiểm tra, bất kể tôi khai báo nó ở đâu. Nó cũng có nghĩa là không có phụ thuộc thời gian giữa các bài kiểm tra. Vì vậy, có vẻ như không có lợi thế khi sử dụng setUp (). Tuy nhiên, Câu hỏi thường gặp về JUnit có nhiều ví dụ khởi tạo một tập hợp trống trong setUp (), vì vậy tôi nghĩ phải có lý do.


2
Hãy lưu ý rằng câu trả lời khác nhau trong JUnit 4 (khởi tạo trong khai báo) và JUnit 3 (sử dụng setUp); đây là gốc rễ của sự nhầm lẫn.
Nils von Barth

Câu trả lời:


99

Nếu bạn đang thắc mắc cụ thể về các ví dụ trong Câu hỏi thường gặp về JUnit, chẳng hạn như mẫu thử nghiệm cơ bản , tôi nghĩ rằng phương pháp hay nhất được giới thiệu ở đó là lớp đang thử nghiệm nên được khởi tạo trong phương thức setUp của bạn (hoặc trong một phương pháp thử nghiệm) .

Khi các ví dụ JUnit tạo một ArrayList trong phương thức setUp, tất cả chúng sẽ tiếp tục kiểm tra hành vi của ArrayList đó, với các trường hợp như testIndexOutOfBoundException, testEmptyCollection, v.v. Quan điểm ở đó là ai đó đang viết một lớp và đảm bảo rằng nó hoạt động đúng.

Bạn có thể nên làm như vậy khi kiểm tra các lớp của riêng mình: tạo đối tượng của bạn trong setUp hoặc trong một phương thức kiểm tra, để bạn có thể có được đầu ra hợp lý nếu bạn phá vỡ nó sau này.

Mặt khác, nếu bạn sử dụng lớp bộ sưu tập Java (hoặc lớp thư viện khác, cho vấn đề đó) trong mã thử nghiệm của mình, có thể không phải vì bạn muốn thử nghiệm nó - nó chỉ là một phần của quá trình thử nghiệm. Trong trường hợp này, bạn có thể yên tâm cho rằng nó hoạt động như dự định, vì vậy việc khởi tạo nó trong khai báo sẽ không thành vấn đề.

Đối với những gì nó đáng giá, tôi làm việc trên một cơ sở mã được phát triển bởi TDD khá lớn, vài năm tuổi. Chúng tôi thường khởi tạo mọi thứ trong phần khai báo của chúng bằng mã thử nghiệm và trong năm rưỡi tôi tham gia dự án này, nó chưa bao giờ gây ra sự cố. Vì vậy, có ít nhất một số bằng chứng giai thoại rằng đó là một điều hợp lý để làm.


45

Tôi bắt đầu tự đào và tìm thấy một lợi thế tiềm năng của việc sử dụng setUp(). Nếu có bất kỳ ngoại lệ nào được ném ra trong quá trình thực thi setUp(), JUnit sẽ in ra một dấu vết ngăn xếp rất hữu ích. Mặt khác, nếu một ngoại lệ được đưa ra trong quá trình xây dựng đối tượng, thông báo lỗi chỉ cho biết JUnit không thể khởi tạo trường hợp thử nghiệm và bạn không thấy số dòng nơi xảy ra lỗi, có thể do JUnit sử dụng phản chiếu để khởi tạo thử nghiệm. các lớp học.

Không điều nào trong số này áp dụng cho ví dụ về việc tạo một tập hợp rỗng, vì điều đó sẽ không bao giờ ném ra, nhưng đó là một lợi thế của setUp()phương pháp.


18

Ngoài câu trả lời của Alex B.

Nó thậm chí còn được yêu cầu sử dụng phương thức setUp để khởi tạo tài nguyên ở một trạng thái nhất định. Thực hiện điều này trong hàm tạo không chỉ là vấn đề về thời gian, mà còn do cách JUnit chạy các bài kiểm tra, mỗi trạng thái kiểm tra sẽ bị xóa sau khi chạy một trạng thái.

JUnit đầu tiên tạo các phiên bản của testClass cho mỗi phương pháp kiểm tra và bắt đầu chạy các bài kiểm tra sau khi mỗi phiên bản được tạo. Trước khi chạy phương pháp thử nghiệm, phương pháp thiết lập của nó được chạy, trong đó một số trạng thái có thể được chuẩn bị.

Nếu trạng thái cơ sở dữ liệu sẽ được tạo trong phương thức khởi tạo, tất cả các phiên bản sẽ khởi tạo trạng thái db ngay sau nhau, trước khi chạy mỗi kiểm tra. Đối với thử nghiệm thứ hai, các thử nghiệm sẽ chạy với trạng thái bẩn.

Vòng đời của JUnits:

  1. Tạo một phiên bản lớp thử nghiệm khác nhau cho mỗi phương pháp thử nghiệm
  2. Lặp lại cho mỗi phiên bản lớp kiểm tra: thiết lập cuộc gọi + gọi testmethod

Với một số đăng nhập trong một bài kiểm tra với hai phương pháp kiểm tra bạn nhận được: (số là mã băm)

  • Tạo phiên bản mới: 5718203
  • Tạo phiên bản mới: 5947506
  • Thiết lập: 5718203
  • TestOne: 5718203
  • Thiết lập: 5947506
  • TestTwo: 5947506

3
Đúng, nhưng lạc đề. Cơ sở dữ liệu về cơ bản là trạng thái toàn cục. Đây không phải là vấn đề mà tôi phải đối mặt. Tôi chỉ quan tâm đến tốc độ thực thi của các bài kiểm tra độc lập thích hợp.
Craig P. Motlin

Thứ tự khởi tạo này chỉ đúng trong JUnit 3, đây là điều cần chú ý. Trong JUnit 4, các trường hợp thử nghiệm được tạo một cách lười biếng, vì vậy việc khởi tạo trong khai báo hoặc trong một phương pháp thiết lập đều xảy ra tại thời điểm thử nghiệm. Cũng cho thiết lập một lần, người ta có thể sử dụng @BeforeClasstrong JUnit 4.
Nils von Barth

11

Trong JUnit 4:

  • Đối với Class Under Test , hãy khởi tạo trong một @Beforephương thức, để bắt lỗi.
  • Đối với các lớp khác , hãy khởi tạo trong khai báo ...
    • ... cho ngắn gọn và để đánh dấu các trường final, chính xác như đã nêu trong câu hỏi,
    • ... trừ khi khởi tạo phức tạp có thể không thành công, trong trường hợp đó sử dụng @Before, để bắt lỗi.
  • Đối với trạng thái toàn cục (đặc biệt là khởi tạo chậm , như cơ sở dữ liệu), hãy sử dụng @BeforeClass, nhưng hãy cẩn thận với sự phụ thuộc giữa các lần kiểm tra.
  • Tất nhiên, việc khởi tạo một đối tượng được sử dụng trong một thử nghiệm đơn lẻ phải được thực hiện trong chính phương pháp thử nghiệm.

Khởi tạo trong một @Beforephương pháp hoặc phương pháp kiểm tra cho phép bạn báo cáo lỗi tốt hơn về các lỗi. Điều này đặc biệt hữu ích để khởi tạo Class Under Test (mà bạn có thể phá vỡ), nhưng cũng hữu ích để gọi các hệ thống bên ngoài, như quyền truy cập hệ thống tệp ("không tìm thấy tệp") hoặc kết nối với cơ sở dữ liệu ("kết nối bị từ chối").

thể chấp nhận được tiêu chuẩn đơn giản và luôn sử dụng @Before(lỗi rõ ràng nhưng dài dòng) hoặc luôn khởi tạo trong khai báo (ngắn gọn nhưng gây ra lỗi khó hiểu), vì các quy tắc mã hóa phức tạp khó tuân theo và đây không phải là vấn đề lớn.

Khởi tạo trong setUplà một di tích của JUnit 3, nơi tất cả các phiên bản thử nghiệm đều được khởi chạy một cách háo hức, điều này gây ra sự cố (tốc độ, bộ nhớ, cạn kiệt tài nguyên) nếu bạn thực hiện khởi tạo tốn kém. Vì vậy, thực tiễn tốt nhất là thực hiện khởi tạo tốn kém setUp, chỉ được chạy khi kiểm tra được thực hiện. Điều này không còn được áp dụng nữa, vì vậy nó ít cần thiết hơn để sử dụng setUp.

Điều này tóm tắt một số câu trả lời khác chôn vùi sự dẫn dắt, đáng chú ý là của Craig P. Motlin (tự đặt câu hỏi và tự trả lời), Moss Collum (lớp đang kiểm tra) và dsaff.


7

Trong JUnit 3, trình khởi tạo trường của bạn sẽ được chạy một lần cho mỗi phương pháp thử nghiệm trước khi chạy bất kỳ thử nghiệm nào . Miễn là giá trị trường của bạn nhỏ trong bộ nhớ, mất ít thời gian thiết lập và không ảnh hưởng đến trạng thái toàn cục, thì về mặt kỹ thuật, sử dụng bộ khởi tạo trường là tốt. Tuy nhiên, nếu những điều đó không được giữ, bạn có thể sẽ tốn rất nhiều bộ nhớ hoặc thời gian thiết lập các trường của mình trước khi chạy thử nghiệm đầu tiên và thậm chí có thể hết bộ nhớ. Vì lý do này, nhiều nhà phát triển luôn đặt giá trị trường trong phương thức setUp (), nơi nó luôn an toàn, ngay cả khi nó không thực sự cần thiết.

Lưu ý rằng trong JUnit 4, việc khởi tạo đối tượng thử nghiệm xảy ra ngay trước khi chạy thử nghiệm và do đó, sử dụng trình khởi tạo trường an toàn hơn và kiểu được khuyến nghị.


Hấp dẫn. Vì vậy, hành vi bạn mô tả lúc đầu chỉ áp dụng cho JUnit 3?
Craig P. Motlin

6

Trong trường hợp của bạn (tạo danh sách) không có sự khác biệt trong thực tế. Nhưng nói chung tốt hơn là sử dụng setUp (), vì điều đó sẽ giúp Junit báo cáo các Ngoại lệ một cách chính xác. Nếu một ngoại lệ xảy ra trong phương thức khởi tạo / khởi tạo của Kiểm thử, đó là lỗi kiểm tra . Tuy nhiên, nếu một ngoại lệ xảy ra trong quá trình thiết lập, điều tự nhiên là bạn nên nghĩ đó là một số vấn đề trong việc thiết lập thử nghiệm và junit báo cáo nó một cách thích hợp.


1
nói hay lắm. Chỉ cần quen với việc luôn khởi tạo trong setUp () và bạn có một câu hỏi ít phải lo lắng hơn - ví dụ: tôi nên khởi tạo fooBar ở đâu, bộ sưu tập của tôi ở đâu. Đó là một loại tiêu chuẩn mã hóa mà bạn chỉ cần tuân thủ. Mang lại lợi ích cho bạn không phải với danh sách, mà với các bản thuyết minh khác.
Olaf Kock

@Olaf Cảm ơn vì thông tin về tiêu chuẩn mã hóa, tôi đã không nghĩ về điều đó. Mặc dù vậy, tôi có xu hướng đồng ý với ý tưởng của Moss Collum về một tiêu chuẩn mã hóa.
Craig P. Motlin 09/02/09

5

Tôi thích khả năng đọc trước tiên mà hầu hết thường không sử dụng phương pháp thiết lập. Tôi đưa ra một ngoại lệ khi thao tác thiết lập cơ bản mất nhiều thời gian và được lặp lại trong mỗi lần kiểm tra.
Tại thời điểm đó, tôi chuyển chức năng đó sang một phương pháp thiết lập bằng cách sử dụng @BeforeClasschú thích (tối ưu hóa sau).

Ví dụ về tối ưu hóa bằng @BeforeClassphương pháp thiết lập: Tôi sử dụng dbunit cho một số thử nghiệm chức năng cơ sở dữ liệu. Phương thức thiết lập có nhiệm vụ đưa cơ sở dữ liệu vào trạng thái đã biết (rất chậm ... 30 giây - 2 phút tùy thuộc vào lượng dữ liệu). Tôi tải dữ liệu này trong phương pháp thiết lập được chú thích bằng @BeforeClassvà sau đó chạy 10-20 thử nghiệm với cùng một tập dữ liệu thay vì tải lại / khởi tạo cơ sở dữ liệu bên trong mỗi thử nghiệm.

Sử dụng Junit 3.8 (mở rộng TestCase như được hiển thị trong ví dụ của bạn) yêu cầu viết nhiều mã hơn một chút so với chỉ thêm chú thích, nhưng vẫn có thể "chạy một lần trước khi thiết lập lớp".


1
+1 vì tôi cũng thích tính dễ đọc hơn. Tuy nhiên, tôi không tin rằng cách thứ hai là tối ưu hóa chút nào.
Craig P. Motlin

@Motlin Tôi đã thêm ví dụ dbunit để làm rõ cách bạn có thể tối ưu hóa khi thiết lập.
Alex B

Cơ sở dữ liệu về cơ bản là trạng thái toàn cục. Vì vậy, việc di chuyển thiết lập db sang setUp () không phải là tối ưu hóa, nó cần thiết để các bài kiểm tra hoàn thành đúng cách.
Craig P. Motlin

@Alex B: Như Motlin đã nói, đây không phải là một sự tối ưu hóa. Bạn chỉ đang thay đổi vị trí trong mã mà quá trình khởi tạo được thực hiện, nhưng không phải là bao nhiêu lần cũng như không nhanh như thế nào.
Eddie

Tôi có ý định sử dụng chú thích "@BeforeClass". Chỉnh sửa ví dụ để làm rõ.
Alex B

2

Vì mỗi bài kiểm tra được thực thi độc lập, với một phiên bản mới của đối tượng, không có nhiều điểm để đối tượng Kiểm tra có bất kỳ trạng thái bên trong nào ngoại trừ được chia sẻ giữa setUp()và một bài kiểm tra riêng lẻ và tearDown(). Đây là một lý do (ngoài những lý do mà những người khác đưa ra) rằng bạn nên sử dụng setUp()phương pháp này.

Lưu ý: Đối tượng thử nghiệm JUnit duy trì trạng thái tĩnh là một ý tưởng tồi! Nếu bạn sử dụng biến tĩnh trong các thử nghiệm của mình cho bất kỳ mục đích nào khác ngoài mục đích theo dõi hoặc chẩn đoán, bạn đang làm mất hiệu lực một phần mục đích của JUnit, đó là các thử nghiệm có thể (có thể) được chạy theo bất kỳ thứ tự nào, mỗi thử nghiệm chạy với một trạng thái tươi, sạch.

Lợi ích của việc sử dụng setUp()là bạn không phải cắt và dán mã khởi tạo trong mọi phương pháp thử nghiệm và bạn không có mã thiết lập thử nghiệm trong phương thức khởi tạo. Trong trường hợp của bạn, có rất ít sự khác biệt. Chỉ cần tạo một danh sách trống có thể được thực hiện một cách an toàn khi bạn hiển thị nó hoặc trong phương thức khởi tạo vì đó là một lần khởi tạo nhỏ. Tuy nhiên, như bạn và những người khác đã chỉ ra, Exceptionnên thực hiện bất cứ điều gì có thể ném vào setUp()để bạn nhận được kết xuất ngăn xếp chẩn đoán nếu nó không thành công.

Trong trường hợp của bạn, khi bạn chỉ tạo một danh sách trống, tôi sẽ làm giống như cách bạn đề xuất: Gán danh sách mới tại điểm khai báo. Đặc biệt là bởi vì cách này bạn có tùy chọn đánh dấu finalnếu điều này có ý nghĩa đối với lớp kiểm tra của bạn.


1
+1 vì bạn là người đầu tiên thực sự hỗ trợ khởi tạo danh sách trong quá trình xây dựng đối tượng để đánh dấu nó là cuối cùng. Tuy nhiên, nội dung về các biến tĩnh không chủ đề đối với câu hỏi.
Craig P. Motlin 09/02/09

@Motlin: đúng, nội dung về biến tĩnh hơi lạc đề. Tôi không chắc tại sao tôi lại thêm điều đó, nhưng nó có vẻ phù hợp vào thời điểm đó, một phần mở rộng của những gì tôi đã nói trong đoạn đầu tiên.
Eddie

Lợi thế của finalđược đề cập trong câu hỏi mặc dù.
Nils von Barth

0
  • Các giá trị không đổi (sử dụng trong đồ đạc hoặc xác nhận) nên được khởi tạo trong khai báo của chúng và final(không bao giờ thay đổi)

  • đối tượng đang được kiểm tra nên được khởi tạo trong phương thức thiết lập vì chúng tôi có thể thiết lập mọi thứ. Tất nhiên, chúng tôi có thể không đặt một cái gì đó bây giờ nhưng chúng tôi có thể đặt nó sau. Khởi tạo trong phương thức init sẽ dễ dàng thay đổi.

  • các phụ thuộc của đối tượng đang được kiểm tra nếu chúng được chế tạo, thậm chí không nên được tự khởi tạo bởi chính bạn: ngày nay, các khuôn khổ giả có thể khởi tạo nó bằng cách phản chiếu.

Một bài kiểm tra không phụ thuộc vào mô hình có thể trông như sau:

public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Before
    public void beforeEach() {
       some = new Some(new Foo(), new Bar());
    } 

    @Test
    public void populateList()
         ...
    }
}

Một bài kiểm tra với các phụ thuộc để tách biệt có thể trông như sau:

@RunWith(org.mockito.runners.MockitoJUnitRunner.class)
public class SomeTest {

    Some some; //instance under test
    static final String GENERIC_ID = "123";
    static final String PREFIX_URL_WS = "http://foo.com/ws";

    @Mock
    Foo fooMock;

    @Mock
    Bar barMock;

    @Before
    public void beforeEach() {
       some = new Some(fooMock, barMock);
    }

    @Test
    public void populateList()
         ...
    }
}
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.