JUnit Rule là gì?
Rule (quy tắc) trong JUnit 4 là một thành phần cho phép chúng ta viết code để thực hiện một số công việc trước và sau khi phương thức test thực thi. Do đó, tránh duplicate code trong các lớp test khác nhau. Chúng rất hữu ích để thêm nhiều chức năng hơn cho tất cả các phương thức test trong một class test. Chúng ta có thể mở rộng hoặc sử dụng lại các rule được cung cấp hoặc viết các rule mới theo mục đích sử dụng riêng.
Tất cả các lớp Rule của JUnit 4 phải implement một interface org.junit.rules.TestRule. Một số Rule có sẵn trong JUnit 4:
- TemporaryFolder Rule
- Timeout Rule
- ExternalResource Rules và ClassRule
- ErrorCollector Rule
- Verifier Rule :
- TestWatchman/TestWatcher Rules
- TestName Rule
- ExpectedException Rules
- RuleChain
- Custom Rules
Ví dụ sử dụng JUnit Rule
TemporaryFolder Rule
TemporaryFolder Rule cho phép chúng ta tạo các file và folder. Các file và folder này sẽ bị xóa cho dù phương thức test thành công hay thất bại ngay sau khi phương thức test kết thúc. Theo mặc định, không có ngoại lệ được ném nếu tài nguyên không thể bị xóa. Vì vậy, nếu cần chạy test một file hoặc folder tạm thời thì có thể sử dụng quy tắc này.
Ví dụ:
package com.gpcoder.junit.rule; import static org.junit.Assert.assertTrue; import java.io.File; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; public class TemporaryFolderRuleTest { /** * Annotates fields that reference rules or methods that return a rule. * * A field must be public, not static, and a sub-type of * org.junit.rules.TestRule (preferred) or org.junit.rules.MethodRule. * * A method must be public, not static, and must return a sub-type of * org.junit.rules.TestRule (preferred) or org.junit.rules.MethodRule. */ @Rule public TemporaryFolder tempFolder = new TemporaryFolder(); @Test public void testFile() throws Exception { File testFolder = tempFolder.newFolder("TestFolder"); File testFile = tempFolder.newFile("test.txt"); assertTrue(testFolder.exists()); assertTrue(testFile.exists()); } }
Timeout Rule
ExternalResource Rules áp dụng cùng thời gian chờ (timeout) cho tất cả các phương thức test trong một class.
package com.gpcoder.junit.rule; import java.util.concurrent.TimeUnit; import org.junit.Rule; import org.junit.Test; import org.junit.rules.Timeout; public class TimeoutRuleTest { /** * Creates a Timeout that will timeout a test after the given duration, in * milliseconds. */ @Rule public Timeout timeout = Timeout.millis(3000); @Test public void testA() throws Exception { try { // Do normal task TimeUnit.MILLISECONDS.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } @Test public void testB() throws Exception { try { // Do heavy task TimeUnit.SECONDS.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } }
Kết quả test:
ExternalResource Rules và ClassRule
ExternalResource là lớp cơ sở cho Rule tương tự như TemporaryFolder, nó cho phép thiết lập tài nguyên bên ngoài trước khi thực thi phương thức test (file, server, database connection, 3rd party serivce,…) và đảm bảo tài nguyên được giải phóng sau đó (teardown). Rule này thích hợp cho Integration Test (kiểm tra tích hợp).
Với annotation ClassRule, chúng ta có thể mở rộng hoạt động của InternalResource sang nhiều lớp. Nó rất hữu ích khi chúng ta cần lặp lại test setup/ teardown cho nhiều lớp. Ví dụ: nếu chúng ta đang thực hiện Integration Test và chúng ta phải kết nối database trước khi kiểm tra và đóng kết nối sau kiểm tra, chúng ta nên sử dụng annotation ClassRule.
Annotation ClassRule phải được đánh dấu với public static field.
Ví dụ:
Giả sử chúng ta có class kết nối đến cơ sở dữ liệu và thực hiện một số thao tác cơ bản như thêm (insert), cập nhật (update), xóa (delete).
DatabaseConnection.java
package com.gpcoder.junit.rule.ExternalResource; public class DatabaseConnection { private static final DatabaseConnection CONNECTION = new DatabaseConnection(); private DatabaseConnection() { } public static DatabaseConnection getConnection() { return CONNECTION; } public void open() { System.out.println("Opened connection to database"); } public void close() { System.out.println("Closed connection to database"); } public boolean insert() { System.out.println("Saved"); return true; } public boolean update() { System.out.println("Updated"); return true; } public boolean delete() { System.out.println("Deleted"); return true; } }
Tiếp theo, chúng ta tạo một class kế thừa từ ExternalResource, class này sẽ mở kết nối/ đóng kết nối đến cơ sở dữ liệu một cách tự động trước khi mỗi class test được thực thi.
DatabaseExternalResource.java
package com.gpcoder.junit.rule.ExternalResource; import org.junit.rules.ExternalResource; /** * A base class for Rules (like TemporaryFolder) that set up an external * resource before a test (a file, socket, server, database connection, etc.), * and guarantee to tear it down afterward */ public class DatabaseExternalResource extends ExternalResource { private static final DatabaseConnection connection = DatabaseConnection.getConnection(); @Override protected void before() throws Throwable { connection.open(); } @Override protected void after() { connection.close(); } public DatabaseConnection getConnection() { return connection; } }
Để sử dụng ExternalResource, chúng ta sẽ thêm vào mỗi test class một field được đánh dấu annotation @ClassRule như sau:
UserDaoTest.java
package com.gpcoder.junit.rule.ExternalResource; import static org.junit.Assert.assertEquals; import org.junit.ClassRule; import org.junit.Test; public class UserDaoTest { /** * When we use @ClassRule, our rule instance should be static, * just like @BeforeClass and @AfterClass methods. */ @ClassRule public static DatabaseExternalResource db = new DatabaseExternalResource(); @Test public void testInsert() { assertEquals(true, db.getConnection().insert()); } @Test public void testUpdate() { assertEquals(true, db.getConnection().update()); } }
RoleDaoTest.java
package com.gpcoder.junit.rule.ExternalResource; import static org.junit.Assert.assertEquals; import org.junit.ClassRule; import org.junit.Test; public class RoleDaoTest { /** * When we use @ClassRule, our rule instance should be static, * just like @BeforeClass and @AfterClass methods. */ @ClassRule public static DatabaseExternalResource db = new DatabaseExternalResource(); @Test public void testInsert() { assertEquals(true, db.getConnection().insert()); } @Test public void testDelete() { assertEquals(true, db.getConnection().delete()); } }
Tiếp theo chúng ta sẽ tạo một Test Suite để thực thi 2 class test trên cùng lúc.
DaoIntegerationTest.java
package com.gpcoder.junit.rule.ExternalResource; import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ UserDaoTest.class, RoleDaoTest.class }) public class DaoIntegerationTest { // the class remains empty, used only as a holder for the above annotations }
Chạy test cho class trên, chúng ta có kết quả như sau:
Opened connection to database Saved Updated Closed connection to database Opened connection to database Deleted Saved Closed connection to database
Kết quả test:
ErrorCollector Rule
ErrorCollector Rule cho phép tiếp tục thực hiện test sau khi tìm thấy sự cố đầu tiên (exception). Nó thu thập tất cả các lỗi và báo cáo tất cả chúng cùng một lúc.
package com.gpcoder.junit.rule; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertEquals; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ErrorCollector; public class ErrorCollectorRuleTest { /** * The ErrorCollector rule allows execution of a test to continue after the * first problem is found (for example, to collect _all_ the incorrect rows in a * table, and report them all at once) */ @Rule public ErrorCollector collector = new ErrorCollector(); @Test public void fails_first_assert_mismatch() { assertEquals(1, 2); // Failed and stop at first error assertEquals(2, 2); // Not running assertEquals(2, 3); // Not running } @Test public void fails_after_execution_1() { /** * Adds a failure to the table if matcher does not match value.Execution * continues, but the test will fail at the end if the match fails. */ collector.checkThat(5, is(8)); // First Error collector.checkThat(5, is(not(8))); // Passed collector.checkThat(5, is(equalTo(9))); // Second Error } @Test public void fails_after_execution_2() { /** * Adds a Throwable to the table. Execution continues, but the test will fail at * the end. */ collector.addError(new Throwable("first thing went wrong")); collector.checkThat(1, is(1)); collector.checkThat(1, is(2)); } }
Kết quả test:
Verifier Rule
Verifier Rule thực hiện kiểm tra verfify và nếu thất bại thì phương thức test sẽ kết thúc với kết quả không thành công (fail). Chúng ta có thể viết logic để verfify riêng của mình với Verifier Rule.
Ví dụ:
import org.junit.Rule; import org.junit.Test; import org.junit.rules.Verifier; public class VerifierRuleTest { private List<String> errorLog = new ArrayList<String>(); /** * Verifier is a base class for Rules like ErrorCollector, which can turn * otherwise passing test methods into failing tests if a verification check is * failed */ @Rule public Verifier verifier = new Verifier() { // After each method perform this check @Override public void verify() { assertTrue("Error Log is not Empty!", errorLog.isEmpty()); } }; @Test public void test1() { // Do something with an error and write to log errorLog.add("There is an error!"); } @Test public void test2() { // Do something with an error and write to log errorLog.add("There is an error!"); } @Test public void test3() { // Success } }
Kết quả test:
TestWatchman/TestWatcher Rules
TestWatcher (TestWatchman không dùng nữa) là các lớp cho Rules và nó cho phép theo dõi (watch) các phương thức test và ghi log cho mỗi phương thức test pass hoặc fail.
Ví dụ:
package com.gpcoder.junit.rule; import org.junit.Assert; import org.junit.FixMethodOrder; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runners.MethodSorters; import org.junit.runners.model.Statement; @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class TestWatcherRuleTest { private static StringBuilder watchedLog = new StringBuilder(); @Rule public TestRule watchman = new TestWatcher() { @Override public Statement apply(Statement base, Description description) { return super.apply(base, description); } @Override protected void succeeded(Description description) { watchedLog.append(description.getDisplayName() + " " + "success!\n"); System.out.println("Succeed! Watchlog:\n" + watchedLog); } @Override protected void failed(Throwable e, Description description) { watchedLog.append(description.getDisplayName() + " " + e.getClass().getSimpleName() + "\n"); System.out.println("Failed! Watchlog:\n" + watchedLog); } @Override protected void starting(Description description) { super.starting(description); System.out.println("Starting test! Watchlog:\n" + watchedLog); } @Override protected void finished(Description description) { super.finished(description); System.out.println("Test finished! Watchlog:\n" + watchedLog + "\n---\n"); } }; @Test public void T1_succeeds() { Assert.assertEquals(5, 5); } @Test public void T2_succeeds2() { Assert.assertEquals(2, 2); } @Test public void T3_fails() { Assert.assertEquals(3, 5); } }
Chạy class test trên, chúng ta có kết quả như sau:
Starting test! Watchlog: Succeed! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! Test finished! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! --- Starting test! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! Succeed! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T2_succeeds2(com.gpcoder.junit.rule.TestWatcherRuleTest) success! Test finished! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T2_succeeds2(com.gpcoder.junit.rule.TestWatcherRuleTest) success! --- Starting test! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T2_succeeds2(com.gpcoder.junit.rule.TestWatcherRuleTest) success! Failed! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T2_succeeds2(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T3_fails(com.gpcoder.junit.rule.TestWatcherRuleTest) AssertionError Test finished! Watchlog: T1_succeeds(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T2_succeeds2(com.gpcoder.junit.rule.TestWatcherRuleTest) success! T3_fails(com.gpcoder.junit.rule.TestWatcherRuleTest) AssertionError ---
TestName Rule
TestName Rule cho phép chúng ta có lấy được tên của phương thức test hiện tại bên trong phương thức test.
Ví dụ:
package com.gpcoder.junit.rule; import static org.junit.Assert.assertEquals; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; public class NameRuleTest { @Rule public TestName name = new TestName(); @Test public void test1() { assertEquals("test1", name.getMethodName()); } @Test public void test2() { assertEquals("test2", name.getMethodName()); } }
ExpectedException Rules
ExpectedException Rules cho phép test các loại ngoại lệ và thông báo ngoại lệ mong muốn bên trong phương thức test.
Ví dụ:
package com.gpcoder.junit.rule; import static org.junit.Assert.assertTrue; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; public class ExpectedExceptionRuleTest { @Rule public final ExpectedException thrown = ExpectedException.none(); @Test public void throwsNothing() { assertTrue(true); } @Test public void testThrowsNullPointerExceptionWithMessage() { // Verify that your code throws an exception that is an instance of specific type. thrown.expect(NullPointerException.class); // Verify that your code throws an exception whose message contains a specific text. thrown.expectMessage("The given value cannot be null"); // Do something to throw an NullPointerException throw new NullPointerException("The given value cannot be null"); } }
Custom Rules
Để viết một Custom Rules, chúng ta cần implement một interface TestRule. Interface này chỉ có phương thức apply(Statement, Description) trả về một thể hiện của Statement. Câu lệnh Statement.evaluate() sẽ được Junit runtime gọi khi class test thực thi.
Ví dụ chúng ta sẽ tạo một custom Rule để chụp lại màn hình ngay sau khi có một test case bị throw exception.
ScreenshotRule.java
package com.gpcoder.junit.rule.custom; import java.io.IOException; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class ScreenshotRule implements TestRule { public Statement apply(final Statement base, final Description description) { return new Statement() { @Override public void evaluate() throws Throwable { System.out.println("Before test"); try { base.evaluate(); } catch (Throwable t) { takeScreenshot(); throw t; // Report failure to JUnit } finally { System.out.println("After test"); } } private void takeScreenshot() throws IOException { System.out.println("Take a screen shot and save to image file"); } }; } }
ScreenshotRuleTest.java
package com.gpcoder.junit.rule.custom; import java.io.IOException; import org.junit.Rule; import org.junit.Test; public class ScreenshotRuleTest { @Rule public ScreenshotRule screenshotRule = new ScreenshotRule(); @Test public void testScreenShot() throws IOException { throw new IOException("Application is crashed"); } }
Kết quả test:
Before test Take a screen shot and save to image file After test
RuleChain
Một class có thể có rất nhiều Rule. Tuy nhiên, các Rule có thể được thực thi theo bất kỳ thứ tự nào, chúng ta không thể dựa vào thứ tự khai báo làm lệnh thực thi.Thay vào đó, chúng ta có thể tạo sử dụng RuleChain để sắp xếp thứ tự thực thi các Rule như chúng ta cần.
Ví dụ:
package com.gpcoder.junit.rule.chain; import static org.junit.Assert.assertTrue; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; import org.junit.rules.TestRule; public class RuleChainTest { @Rule public TestRule chain = RuleChain // Returns a RuleChain with a single TestRule. This method is the usual starting // point of a RuleChain. .outerRule(new LoggingRule("outer rule")) // Create a new RuleChain, which encloses the nextRule with the rules of the // current RuleChain. .around(new LoggingRule("middle rule")) .around(new LoggingRule("inner rule")); @Test public void test() { assertTrue(true); } }
Kết quả test:
Starting: outer rule Starting: middle rule Starting: inner rule Finished: inner rule Finished: middle rule Finished: outer rule
Tài liệu tham khảo: