49 關於Spring集成單元測試

前言 

呵呵 這個還是準備了一陣子了, 應該有 兩週了吧, 不過 最近比較忙 

主要是, 我們經常使用到單元測試, 然後其中又有一些比較特殊的用法, 感覺到有些不可思議, 然後 花了一些時間跟蹤了一下 相關的代碼, 在這個過程中也瞭解到了一些 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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章