前言
呵呵 這個還是準備了一陣子了, 應該有 兩週了吧, 不過 最近比較忙
主要是, 我們經常使用到單元測試, 然後其中又有一些比較特殊的用法, 感覺到有些不可思議, 然後 花了一些時間跟蹤了一下 相關的代碼, 在這個過程中也瞭解到了一些 junit 留下來的可擴展的東西(TestRule, MethodRule, TestExecutionListener 等等), @RunWith, @ContextConfiguration是幹什麼的, @Sql是如何執行的 等等
比如, 如下代碼片爲什麼能夠測試通過 ^_^
thrown.expectMessage(" Exception in bookService.update ");
System.out.println(" bookService.update ");
throw new RuntimeException(" there is a Exception in bookService.update ");
我們這裏主要是討論如下幾個問題
一個是 如何獲取 JunitClassRunner, 一個是 如何初始化 ApplicationContext
一個是 ParentRunner.classBlock, 一個是 SpringJunit4ClassRunner.methodBlock
一個是 異常處理, 如何處理的? @Test(expected) 和 @Rule ExpectedException thrown 有什麼區別 ?
以下部分的代碼, 截圖基於 : jdk1.7.40 + junit4.12 + spring-x-4.3.0
測試代碼
/**
* Test01BookServiceTest
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 2019/11/2 18:36
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
//@Sql(scripts = "classpath:classScript.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public class Test01BookServiceTest {
@Autowired
private IBookService bookService;
@ClassRule
public static CostLogRule costLogRule = new CostLogRule();
@Rule
public LoopRule loopRule = new LoopRule(2);
@Rule
public ExpectedException thrown = ExpectedException.none();
// @ClassRule
// public static CostLogRule classRule() {
// return new CostLogRule();
// }
@BeforeClass
public static void beforeClass() {
System.out.println(" before class invoked ");
}
@Before
public void before() {
System.out.println(" before invoked ");
}
@Test(expected = RuntimeException.class)
// @Timed(millis = 3000L)
// @Sql(scripts = "classpath:testAdd.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public void testAdd() {
Book entity = new Book();
entity.setAuthor("jerry.x.he");
entity.setName("演員的自我修養");
bookService.add(entity);
throw new RuntimeException(" for expected ");
}
@Test
public void testUpdate() {
thrown.expectMessage(" Exception in bookService.update ");
thrown.expect(new Matcher<Object>() {
@Override
public boolean matches(Object o) {
return o instanceof RuntimeException;
}
@Override
public void describeMismatch(Object o, org.hamcrest.Description description) {
}
@Override
public void _dont_implement_Matcher___instead_extend_BaseMatcher_() {
}
@Override
public void describeTo(org.hamcrest.Description description) {
}
});
System.out.println(" bookService.update ");
throw new RuntimeException(" there is a Exception in bookService.update ");
}
@After
public void after() {
System.out.println(" after invoked ");
}
@AfterClass
public static void afterClass() {
System.out.println(" after class invoked ");
}
}
/*
* 用於循環執行測試的Rule,在構造函數中給定循環次數。
* refer : https://www.iteye.com/blog/haibin369-2088541
*/
class LoopRule implements TestRule {
private int loopCount;
public LoopRule(int loopCount) {
this.loopCount = loopCount + 1;
}
@Override
public Statement apply(final Statement base, Description description) {
return new Statement() {
//在測試方法執行的前後分別打印消息
@Override
public void evaluate() throws Throwable {
for (int i = 1; i < loopCount; i++) {
System.out.println("Loop " + i + " started!");
base.evaluate();
System.out.println("Loop "+ i + " finished!");
}
}
};
}
}
/**
* CostLogRule
*
* @author Jerry.X.He <[email protected]>
* @version 1.0
* @date 2019/11/3 10:59
*/
class CostLogRule implements TestRule {
@Override
public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
long start = System.currentTimeMillis();
base.evaluate();
long spent = System.currentTimeMillis() - start;
System.out.println(description + " spent " + spent + " ms ");
}
};
}
}
1. ParentRunner.classBlock
1. childrenInvoker : 主要是封裝的 調用所有的需要處理的業務單元測試
2. withBeforeClasses, withAfterClasses : 主要是採集 @BeforeClass, @AfterClass 需要執行的方法列表, 封裝 RunBefore/AfterTestClassCallbacks[taskManager.before/afterTestClass], RunBefores/Afters[裏面記錄了採集的@BeforeClass, @AfterClass的方法列表]
3. withClassRules : 獲取測試類上面的 @ClassRule static TestRule xxMethod(); 相關的方法並執行, 獲取 TestRule, 獲取測試類上面的 @ClassRule static TestRule xxField; 添加到 TestRule 列表, 封裝一個 RunRules
RuleRules 初始化的時候 apply 所有的 TestRule, 構造一個新的 RunRules 需要委託的 Statement, 對應於我們這裏的場景就是 CostLogRule$1 的內部類的一個實例
假設我們這裏 @BeforeClass, @AfterClass, @ClassRule 相關的特性都是用到了, 那麼, 我們得到的一個 Statement 是 RunRules(CostLogRule$1(RunAfterTestClassCallbacks(RunAfters(RunBeforeTestClassCallbacks(RunBefores($childrenInvoker))))))
一層 Statement 委託下一層 Statement, 最底層的 Statement 爲處理單元測試業務的 Statement, 然後一個 evaluate 調用下去, 其實就是 典型的靜態代理的封裝
靜態代理 和 責任鏈
和責任鏈不同的是, 責任鏈裏面的每一環只需要關心 自己的業務 和 責任鏈這個Context, 而不用關心其他的業務接口對象
這裏沒有了 Context, 有的只是下一個業務接口, 需要關心的是自己的業務 和 下一個業務接口對象
一連串調用之後, 最後回來到 childrenInvoker 的業務, 也就是調用各個單元測試方法, 每一個測試用例, 會調用 SpringJunit4ClassRunner.runChild, 這裏面又是 方法這個層面的 Statement 的一系列封裝
2. SpringJunit4ClassRunner.methodBlock
1. methodInvoker : 主要是封裝的是 調用某一個具體的單元測試的業務, 封裝了一個 InvokeMethod, 裏面記錄了方法調用相關的上下文需要的信息, 方法, 調用方法的測試對象
2. possiblyExpectingExceptions : 如果 @Test(expected = xxException), 則封裝一個 ExpectException 的一個 Statement, 來處理 期望發生指定異常 相關業務, 不過這裏粒度似乎是還是太粗了, 只能精確到 異常類型, 不能獲取到 異常的上下文的信息
3. withBefores, withAfters : 主要是採集 @Before, @After 需要執行的方法列表, 封裝 RunBefore/AfterTestMethodCallbacks[taskManager.before/afterTestMethod], RunBefores/Afters[裏面記錄了採集的@Before, @After的方法列表]
4. withRulesReflectively : 調用當前 Runner 的 withRules 來處理 Rule 相關業務, 我們這裏是 獲取 MethodRule 相關方法, 字段實例, 相關來處理 baseStatement, 然後 獲取 @Rule 相關方法, 字段實例, 來構造 RunRules
對應於我們這裏 沒有 MethodRule, 所以 暫時不考慮 MethodRule
RuleRules 初始化的時候 apply 所有的 TestRule, 構造一個新的 RunRules 需要委託的 Statement, 對應於我們這裏的場景就是 先是一個 LoopRule, 後是一個 ExpectedException, 因此得到的是一個 RunRules(ExpectedException(LoopRule($????Statement)))
5. withPotentialRepeat : 封裝一層來處理單元測試的重試 @Repeat 註解[我們這裏的自定義的 LoopRule 其實就是 SpringRepeat 的模仿實現]
6. withPotentialTimeout : 封裝超時相關處理, 有兩種配置 一種是基於 Spring 的註解 @Timed, 另外一種是 junit 自身的 @Test(timeout = xx)
兩種不同的方式, 實現上也有一些區別
SpringFailOnTimeout 是等待業務執行完成之後, 判斷是否超時, 超時則拋出異常
FailOnTimeout 則是子線程執行業務, 主線程等待 timeout 的時間, 如果沒有獲取到結果, 拋出異常
假設我們這裏 @Test(expected), @Before, @After, @Rule, @Repeat, @Timed 相關的特性都是用到了, 那麼, 我們得到的一個 Statement 是 SpringFailOn/FailOnTimeout(SpringRepeat(RunRules(ExpectedException(LoopRule(RunAfterTestMethodCallbacks(RunAfters(RunBeforeTestMethodCallbacks(RunBefores(ExpectException($methodInvoker))))))))))
在業務方法 Test01BookServiceTest.testAdd 打一個斷點
縱觀一下這裏的兩個層級, 調用方法層面上的 methodInvoker 和 調用類層面上的 childrenInvoker, 如下圖所示
紅框處標記爲 兩個不同的層級的 Statement 委託鏈
3. 如何獲取 JunitClassRunner
在 JUnit4IdeaTestRunner. startRunnerWithArgs 中代碼如下, 反編譯自 jd-gui.exe
繼續 ClassRequest. getRunner 往下看
tips : 關於 TestContextManager 的初始化 testExecutionListeners
默認的 TaskExecutionListener 來自於 spring-test.jar 裏面的 spring.factories, key 爲 org.springframework.test.context.TestExecutionListener, 實例化對應的 Listener 是簡單粗暴的反射
4. 如何初始化 ApplicationContext
創建了 TestClass 對應的實例之後, 需要初始化給定的實例, 在 DependencyInjectionTestExecutionListener.prepareTestInstance 的時候, 會創建 ApplicationContext
mergedContextConfiguration 在我們這裏的場景, 來自於 @ContextConfiguration, 具體可以參見 AbstractTestContextBootstrapper. buildTestContext
從 AbstractDelegatingSmartContextLoader.supports 方法中可以知道, 是支持 Xml 和 註解兩種方式來加載 ApplicationContext, 我們這裏配置的是 "classpath:applicationContext.xml", 因此我們這裏是根據 xml 來加載的 ApplicationContext
然後是 初始化 我們的 TestClass 的實例, 看這裏的顯式注入
5. @Test(expected) 和 @Rule ExpectedException thrown 有什麼區別 ?
1. @Test(expected = xxException.class) 對應的 handler 如下
可以看到, 僅僅是簡單的 catch 了一下對應的異常, 期望 xxException.class 拋出, 沒有拋出, 或者拋出的不是 xxException 的實例, 會拋出 Exception, AssertionError
2. @Rule ExpectedException thrown 的相關處理
可以看到這裏是 handleException 封裝了一層業務處理, 然後是 根據 matchBuilder 封裝了一個 組合的 Matcher, 來處理 match 業務
二者相比, 自然是 thrown 對應的斷言功能更加強大一些, 輸入了 整個異常的當下文留給 Matcher 去處理
完
引用
自定義 TestRule 部分 : https://www.iteye.com/blog/haibin369-2088541