Mục lục
Một test class thường có nhu cầu lưu trữ dữ liệu chung áp dụng cho tất cả các test method như là data giả lập, dữ liệu từ database, etc. Mặc định JUnit 5 sẽ khởi tạo một instance của test class trước khi thực thi một test method. Điều này sẽ giúp tách biệt trạng thái và tránh xung đội dữ liệu giữa các test method.
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu cách sử dụng @TestInstance để sửa đổi vòng đời của 1 test class.s
Vòng đời mặc định của test class
Hãy xem ví dụ đơn giản dưới đây
class AdditionTest { private int sum = 1; @Test void addingTwoReturnsThree() { sum += 2; assertEquals(3, sum); } @Test void addingThreeReturnsFour() { sum += 3; assertEquals(4, sum); } }
Đây là một test class đơn giản trong JUnit 5, để ý rằng test method không chứa từ khoá public nhưng chúng vẫn hợp lệ vì JUnit 5 không bắt buộc public trong method.
Các test method addingTwoReturnsThree() và addingThreeReturnsFour() đều sữa đổi giá trị của biến sum tuy nhiên cả 2 đều passed bởi vì một instance AdditionTest được tạo trước mỗi test method bắt đầu thực thi, chính vì vậy giá trị của sum luôn là 1 khi addingTwoReturnsThree() và addingThreeReturnsFour() bắt đầu thực thi.
Tạo giá trị không thay đổi qua từng test method
Đôi lúc chúng ta cần một object tồn tại xuyên suốt qua các test method mà không phải khởi tạo lại. Hãy tưởng tại chúng ta cần đọc 1 file csv với khối lượng dữ liệu lớn hoặc một collection các record từ database etc. Nếu các dữ liệu này được đọc lại từ đầu khi một test method mới thực thi thì sẽ tốn rất nhiều thời gian và không có lợi cho hệ thống.
public class TestInstanceExample { private static String largeContent; @BeforeAll public static void setUpFixture() { largeContent = "https://shareprogramming.net//"; } @Test public void testContainNet() { assertTrue(largeContent.contains("net")); } @Test public void testStartWithHTTP() { assertTrue(largeContent.startsWith("http")); } }
Để tránh trường hợp trên, mình đã tạo một biến static largeContent và khởi tạo nó trong @BeforeAll method để đảm bảo rằng tài nguyên này sẽ tồn tại xuyên suốt các test method.
Tuy nhiên chúng ta chúng ta có một cách khác là sử dụng @TestInstance PRE_CLASS mà chúng ta sẽ tìm hiểu ngay sau đây.
@TestInstance annotation
@TestInstance annotation cho phép chúng ta tuỳ biến vòng đời của test class trong JUnit 5.
@TestInstance có 2 cơ chế chính
- LifeCycle.PER_METHOD
- LifeCycle.PER_CLASS
PER_METHOD là chế độ mặc định mà JUnit 5 sẽ khởi tạo một instance mới trước khi thực thi 1 test method để tránh xung đột trạng thái giữa các test method với nhau.
PER_CLASS là cơ chế mới yêu cầu JUnit 5 chỉ được khởi tạo một instance của test class giữa các lần thực thi test method.
@TestInstance(Lifecycle.PER_CLASS) public class TestInstanceExample { private String largeContent; @BeforeAll public void setUpFixture() { largeContent = "https://shareprogramming.net//"; } @Test public void testContainNet() { assertTrue(largeContent.contains("net")); } @Test public void testStartWithHTTP() { assertTrue(largeContent.startsWith("http")); } }
Có thể thấy, khi sử dụng @TestInstance(LifeCylcle.PER_CLASS) @BeforeAll method không bắt buộc phải là static method, và largeContent cũng chỉ được khởi tạo một lần duy nhất tại @BeforeAll method.
Tranh cãi xung quanh @TestInstance(PER_CLASS )
Việc sử dụng @TestInstance(PER_CLASS) có một số lợi ích đối với các class cần thiết lập nhiều tài nguyên tiêu tốn nhiều thời gian, giảm độ phức tạp của test class khi phải sử dụng biến static và biến instance chung với nhau.
Tuy nhiên việc sử dụng chung các tài nguyên thường là điều tối kỵ trong các mẫu pattern, nhưng nó sẽ có ích trong integration test. Nhưng có 1 thách thức là đôi khi một số test method cần làm sử dụng giá trị ban đầu không bị ảnh hưởng bởi các test method trước đó, và 1 số khác lại cần nó được duy trì trong suốt toàn bộ các test method. Trong trường hợp này chúng ta có thể sử dụng @BeforeEach và @AfterEach method để có các thao tác tương ứng với các trường hợp như là reset, xoá, cập nhật giá trị cho các biến etc.
Tóm lược
Qua bài viết này chúng ta đã tìm hiểu xong cách sử dụng @TestInstance để tuỳ biến vòng đời của 1 test class. Thế nhưng việc sử dụng nó cũng gây ra rất nhiều tranh cãi, cá nhân mình thấy @TestInstance(PER_CLASS) sẽ khiến các unit test trở nên phức tạp khi mà giá trị của các tài nguyên ban đầu có thể bị thay đổi ở bất kỳ một test method này, khiến chúng ta cũng phải quan tâm đến các test method khác, đôi lúc sai mà không biết do đâu. Nên dùng @TestInstance(PER_CLASS) trong integration test khi mà chúng ta đã có kịch bản rõ ràng và nắm được các trạng thái của tài nguyên qua mỗi giai đoạn.
Nếu các bạn gặp khó khăn trong quá trình thực nghiệm, có thể checkout source mình đã soạn sẵn tại github repository.
Tài liệu tham khảo
https://www.baeldung.com/junit-testinstance-annotation
https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle