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:




