47 关于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

 

 

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