单元测试基础知识

Posted aaron-007

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单元测试基础知识相关的知识,希望对你有一定的参考价值。

9.单测工具简介

JUnit

Java用的最多的单测框架,使用非常简单,主流IDE基本都集成了JUnit,具体用法就不介绍了,可以花十分钟看看官方文档http://junit.org/junit4/

TestNg

是一款基于junit框架上的加强版单元测试框架,具备更多更强大的功能,能够执行比较复杂的程序代码,具体用法这里也不介绍了,可以花十分钟看看官方文档http://testng.org/doc/documentation-main.html

Runner

JUnit的Runner是指继承了org.junit.runner.Runner的类,是用来真正执行单测方法的类,JUnit有提供默认的Runner,如org.junit.runners.JUnit4,也可以通过@RunWith注解来指定自定义的Runner,如@RunWith(SpringRunner.class)

Runner的介绍可以参考这篇文章: 
http://www.mscharhag.com/java/understanding-junits-runner-architecture

通过自定义Runner,可以简化单测的开发,例如,在Spring没有提供Runner时,为Spring应用编写单测,需要自己初始化Spring上下文,然后从上下文中获取要测试的Bean,使用起来比较麻烦。

对于自定义Runner来说,一般会继承JUnit的org.junit.runners.BlockJUnit4ClassRunner,BlockJUnit4ClassRunner主要提供了下面的功能:

  • 反射创建单测类实例
  • 找出单测方法(被@Test注解的方法)
  • 其它JUnit注解支持,如@After、@Before等等
  • 反射执行单测方法

SpringRunner通过继承BlockJUnit4ClassRunner,在其基础上提供了自动初始化Spring上下文,单测类中的@Resource等注解的处理,等等一系列方便编写Spring单测的功能。

如果我们想实现在单测方法前后执行一些逻辑,我们除了可以使用@Before注解,还可以通过实现自定Runner来实现:

1.  import org.junit.runners.BlockJUnit4ClassRunner;
2.  import org.junit.runners.model.FrameworkMethod;
3.  import org.junit.runners.model.InitializationError;
4.  import org.junit.runners.model.Statement;
5.   
6.  public class TestRunner extends BlockJUnit4ClassRunner {
7.   
8.      public TestRunner(Class<?> klass) throws InitializationError {
9.          super(klass);
10.                }
11.             
12.                @Override
13.                protected Statement methodBlock(FrameworkMethod method) {
14.                    //FrameworkMethod 是单测方法的包装类
15.             
16.                    //获取父类处理的结果,以便使用JUnit提供的注解的功能
17.                    Statement block = super.methodBlock(method);
18.             
19.                    //自定义的逻辑
20.                    Statement newBlock = new Statement() {
21.                        @Override
22.                        public void evaluate() throws Throwable {
23.                            //这里可以在单测方法执行前做一些自定义逻辑
24.                            System.out.println("TestRunner before : " + method.getName());
25.             
26.                            block.evaluate();//单测方法执行,包含@Before等注解处理逻辑
27.             
28.                            //这里可以在单测方法执行后做一些自定义逻辑
29.                            System.out.println("TestRunner after : " + method.getName());
30.                        }
31.                    };
32.             
33.                    return newBlock;
34.                }
35.             
36.            }
37.             
38.            import org.junit.Test;
39.            import org.junit.runner.RunWith;
40.             
41.            @RunWith(TestRunner.class)
42.            public class BTestClass {
43.             
44.                @Test
45.                public void test(){
46.                    System.out.println("test b");
47.                }
48.             
49.            }
50.             
51.            //运行输出
52.            //TestRunner before : test
53.            //test b
54.            //TestRunner after : test
 
在JUnit内部,其实就是用类似的方式来实现@Before、@After等等注解功能的,通过层层的包装Statement类,来实现功能的扩展。

Rule

Rule是JUnit4.7新增加的功能,是JUnit的另一种扩展机制,可以扩展单测方法的执行。上面TestRunner的功能也可以通过Rule机制来实现:

1.  import org.junit.rules.TestRule;
2.  import org.junit.runner.Description;
3.  import org.junit.runners.model.Statement;
4.   
5.  public class EchoRule implements TestRule {
6.   
7.      @Override
8.      public Statement apply(Statement base, Description description) {
9.          Statement newBlock = new Statement() {
10.                        @Override
11.                        public void evaluate() throws Throwable {
12.                            //这里可以在单测方法执行前做一些自定义逻辑
13.                            System.out.println("EchoRule before : " + description.getMethodName());
14.             
15.                            base.evaluate();//单测方法执行,包含@Before等注解处理逻辑
16.             
17.                            //这里可以在单测方法执行后做一些自定义逻辑
18.                            System.out.println("EchoRule after : " + description.getMethodName());
19.                        }
20.                    };
21.                    return newBlock;
22.                }
23.             
24.            }
25.            import org.junit.Rule;
26.            import org.junit.Test;
27.             
28.            public class BTestClass {
29.             
30.                @Rule
31.                public EchoRule rule = new EchoRule();//必需要是public
32.             
33.                @Test
34.                public void test() {
35.                    System.out.println("test b");
36.                }
37.             
38.            }
39.             
40.            //输出
41.            //EchoRule before : test
42.            //test b
43.            //EchoRule after : test

 

可以看到Rule更多的是对单测方法执行前后的一些逻辑的扩展,@Rule注解的属性必需是public的实例属性,如果想在所有单测方法执行前后进行处理(类似@BeforeClass、@AfterClass逻辑),可以通过@ClassRule注解来做到,被@ClassRule的属性必需是static public的属性

Rule机制相对Runner的好处在于,Runner只能指定一个,而一个单测类可以指定多个Rule,Spring也有Rule的实现,在即想使用其它框架的Runner又想使用Spring的单测扩展时,可以使用其它框架的Runner,然后使用Spring的Rule,来组合使用,如:

1.  import org.junit.ClassRule;
2.  import org.junit.Rule;
3.  import org.junit.Test;
4.  import org.junit.runner.RunWith;
5.  import org.mockito.runners.MockitoJUnitRunner;
6.  import org.springframework.test.context.junit4.rules.SpringClassRule;
7.  import org.springframework.test.context.junit4.rules.SpringMethodRule;
8.   
9.  @RunWith(MockitoJUnitRunner.class)
10.            @ContextConfiguration(locations="classpath:spring-root.xml")
11.            public class BTestClass {
12.             
13.                @ClassRule
14.                public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
15.             
16.                @Rule
17.                public final SpringMethodRule springMethodRule = new SpringMethodRule();
18.             
19.                @Test
20.                public void test() {
21.                    System.out.println("test b");
22.                }
23.             
24.            }

 

JUnit自带了一些方便使用的Rule实现,可以参考下面的文档 
https://github.com/junit-team/junit4/wiki/Rules

Mock框架

在真实项目中,往往需要依赖很多外部的接口,如HSF等接口,而我们在运行单测的时候RPC接口可能还未开发完成或者因为环境问题,无法访问,这时我们想要测试自己部分的逻辑,就需要使用到Mock框架,来屏蔽掉外部系统的影响。

使用Mock通常会带来以下一些好处:

  • 隔绝其他模块出错引起本模块的测试错误
  • 隔绝其他模块的开发状态,只要定义好接口,不用管他们开发有没有完成
  • 一些速度较慢的操作,可以用Mock Object代替,使单测快速返回
  • 隔离环境对单测执行的影响,实现在没有外部服务时也能运行单测

常见的Mock框架有EasyMock、Mocktio、JMockit、PowerMock等,个人只简单用过Mocktio和JMockit,就功能上,JMockit的功能更强,能Mock静态方法等,但是根据之前的使用来看,比较难以驾驭,因为JMockit使用的是Java5的Instrumentation机制,会在运行时修改字节码,导致碰到问题时比较难以调试,相对的网络资料也比较少,而Mocktio的使用比较简单明了,因此推荐使用Mocktio

关于Mocktio的使用可以参考文档: 
[https://static.javadoc.io/org.mockito/mockito-core/2.7.22/org/mockito/Mockito.html 
https://juejin.im/entry/578f11aec4c971005e0caf82](https://static.javadoc.io/org.mockito/mockito-core/2.7.22/org/mockito/Mockito.html 
https://juejin.im/entry/578f11aec4c971005e0caf82)

因为Mocktio是使用Cglib来创建代理的,所以对于被Mock的对象来说,要求和Cglib创建代理的要求一样,如不能是final类、不能代理private方法等等限制。

有时候可能需要mock void方法,可以使用下面的方式

1.  OssCache cache = Mockito.mock(OssCache.class);
2.   
3.  Mockito.doNothing().when(cache).putToKey("key", "val");
4.  Mockito.doThrow(new RuntimeException("exp")).when(cache).deleteObj("key");
5.   
6.  cache.putToKey("key", "val");
7.  try {
8.      cache.deleteObj("key");
9.      fail("mast throw exception");
10.            } catch (RuntimeException e) {
11.                assertTrue("exp".equals(e.getMessage()));
12.            }
13.             
14.            Mockito.verify(cache).putToKey("key", "val");
15.            Mockito.verify(cache).deleteObj("key");

 

当Mockito的注解使用起来比较方便,具体注解的使用参见前面链接的文档,Mockito处理注解是通过MockitoAnnotations.initMocks(target)来处理的,而Mockito提供的Runner和Rule其实就是简单的在单测方法执行前执行该行代码,所以可以通过@RunWith(MockitoJUnitRunner.class)或@Rule public MockitoRule mockitoRule = MockitoJUnit.rule()方式来使用Mockito注解功能,当和Spring Test一起使用时,因为一般会使用Spring的Runner,所以可以通过Rule的方式来使用Mockito的注解功能

关于Mockito的大概原理如下 

主要通过ThreadLocal将我们要mock的方法和对应的返回值关联起来

内存数据库

在项目中经常会使用到mysql等数据库,但是在单测运行时,如果访问Mysql等外部服务器,会造成:

  • 单测运行慢
  • 单测运行依赖环境,在无法访问Mysql时,单测无法运行
  • 单测可能会运行的非常频繁,造成Mysql中非常多的垃圾数据
  • 单测依赖数据库中某些特定的数据,造成换个Mysql数据库时单测运行失败

那么如何解决上面的问题,一种方式是Mock掉所有DAO的类,这种方式需要写非常多的Mock,单测写起来比较麻烦,且DAO层面问题无法测试到;另一种方式就是使用内存数据库,内存数据库兼容SQL,启动速度快,数据存放在内存中单测运行后自动丢弃,非常适合单测时使用

常见的内存数据库有很多,但是鉴于单测场景,考虑到安装方便(直接Maven依赖),了解到的有HSQL、H2、Derby等,H2的官网上有个对比表http://www.h2database.com/html/main.html

考虑到目前我们使用的是Mysql数据库,而H2有Mysql模式,对Mysql的语法支持的最好,所以建议使用H2数据库来作为单测数据库,但是H2并不支持所有的Mysql语法,还是有不少的Mysql语法或函数并不支持,对于建表语句而言,可以使用语法转换工具https://github.com/bgranvea/mysql2h2-converter

Spring对嵌入式数据库支持的非常好,可以通过下面的配置来创建嵌入式数据库数据源,同时可以指定初始化表和数据库的脚本

1.  <jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
2.      <jdbc:script location="classpath:/sql/test_schema.sql" />
3.      <jdbc:script location="classpath:/sql/test_init_data.sql" />
4.  </jdbc:embedded-database>

 

具体使用可以参考Spring的文档https://docs.spring.io/spring/docs/current/spring-framework-reference/html/jdbc.html#jdbc-embedded-database-support

除了H2,还可以使用MariaDB4j,MariaDB的Java包装版本(用Java代码安装MariaDB精简版然后启动,比较重) 
https://github.com/vorburger/MariaDB4j

Spring Test介绍

当应用使用了Spring时,编写单测时需要每次手动的初始化Spring上下文,这种方式不仅繁琐,而且不能复用Spring上下文,导致单测执行时间变长,为此,Spring提供了对单测的支持,也就是Spring Test模块

Spring和JUnit的整合,提供了对应的Runner和Rule,我们平常使用的比较多的是Spring的Runner,即SpringJUnit4ClassRunner或者SpringRunner(Spring4.3),Spring的Runner会根据配置自动初始化Spring上下文,并在单测方法执行时对其进行依赖注入,避免手动的getBean操作,简单使用如下

1.  @RunWith(SpringJUnit4ClassRunner.class)
2.  @ContextConfiguration("classpath:spring-test-main.xml")
3.  public class Test {
4.   
5.      @Resource
6.      private SomeBean bean;
7.   
8.      @Test
9.      public void test(){
10.                    String someVal = bean.someMethod();
11.                }
12.             
13.            }

 

Spring Test提供@ContextConfiguration来让我们指定要初始Spring上下文的配置,支持Spring的各种配置方式,如XML、JavaConfig等等方式,@ContextConfiguration和@RunWith等注解都可以注解在基类上,所以可以提供一个基础类来简化单测的编写

1.  @RunWith(SpringJUnit4ClassRunner.class)
2.  @ContextConfiguration("classpath:base-context.xml")
3.  public class XXXTestBase {
4.   
5.  }
6.   
7.  @ContextConfiguration("classpath:extended-context.xml")
8.  public class YYYTest extends XXXTestBase{
9.   
10.            }

 

默认情况下,子类可以继承父类的@ContextConfiguration配置,同时可以追加自己的配置,当程序非常模块化时,可以通过指定特定的配置文件来减少初始化Bean的数量,以便提高单测的执行速度

@TestExecutionListeners注解是Spring Test用来注册TestExecutionListener的注解,提供的类似于JUnit的Before、After的扩展方法,父类的@TestExecutionListeners注解配置通样可以被子类继承,子类也可以提供自己个性的@TestExecutionListeners配置

1.  //可以通过@Order指定TestExecutionListener的顺序
2.  public interface TestExecutionListener {
3.      void beforeTestClass(TestContext testContext) throws Exception;
4.   
5.      void prepareTestInstance(TestContext testContext) throws Exception;
6.   
7.      void beforeTestMethod(TestContext testContext) throws Exception;
8.   
9.      void afterTestMethod(TestContext testContext) throws Exception;
10.             
11.                void afterTestClass(TestContext testContext) throws Exception;
12.            }

 

单测一般不需要显示的配置@TestExecutionListeners注解,默认@ContextConfiguration会自动注册如下Spring Test自带的TestExecutionListener

1.  org.springframework.test.context.web.ServletTestExecutionListener
2.  org.springframework.test.context.support.DependencyInjectionTestExecutionListener
3.  org.springframework.test.context.support.DirtiesContextTestExecutionListener
4.  org.springframework.test.context.transaction.TransactionalTestExecutionListener
5.  org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener

 

可以看到,Spring Test很多方便的功能都是通过TestExecutionListener来实现的,比如说DependencyInjectionTestExecutionListener来为单测类实例进行依赖注入的

1.  public class DependencyInjectionTestExecutionListener extends AbstractTestExecutionListener {
2.   
3.      @Override
4.      public void prepareTestInstance(final TestContext testContext) throws Exception {
5.          if (logger.isDebugEnabled()) {
6.              logger.debug("Performing dependency injection for test context [" + testContext + "].");
7.          }
8.          injectDependencies(testContext);
9.      }
10.             
11.                protected void injectDependencies(final TestContext testContext) throws Exception {
12.                    Object bean = testContext.getTestInstance();
13.                    AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
14.                    beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
15.                    beanFactory.initializeBean(bean, testContext.getTestClass().getName());
16.                    testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
17.                }
18.             
19.            }

 

在单测中经常会使用到Mockito的注解,所以可以在单测基础类中使用Mockito的Rule,这样子类就可以使用Mockito的注解了

1.  @RunWith(SpringJUnit4ClassRunner.class)
2.  @ContextConfiguration("classpath:base-context.xml")
3.  public class XXXTestBase {
4.      @Rule
5.      public MockitoRule rule = MockitoJUnit.rule();
6.  }
7.   
8.  @ContextConfiguration("classpath:extended-context.xml")
9.  public class YYYTest extends XXXTestBase{
10.             
11.                @Resource
12.                @@InjectMocks
13.                private SpringXXXBean xxxBean;
14.             
15.                @Mock
16.                private XXXHsfBean xxxHsfBean
17.             
18.            }

 

在执行单测时,涉及到数据库操作时经常要在单测方法执行前在数据库中准备好单测数据,Spring Test提供了非常方便的注解来在单测方法前初始化数据,如下所示

1.  @RunWith(SpringRunner.class)
2.  @ContextConfiguration(locations = "classpath:spring-test-root.xml")
3.  public class BTestClass {
4.   
5.      @Resource
6.      private TestMapper mapper;
7.   
8.      @Test
9.      @Sql("BTestClass_testAAA.sql")
10.                //默认会在BTestClass相同的目录下查找BTestClass_testAAA.sql文件执行
11.                public void testAAA() {
12.                    List<TestModel> all = mapper.getAll();
13.             
14.                    Assert.isTrue(all != null && all.size() == 1);
15.                }
16.             
17.            }

 

@Sql注解可以注解在类上,表示每个单测方法前都执行该SQL脚本,但是要注意单测方法插入到数据库的记录默认并不会在单测执行完后回滚,所以如果SQL脚本中有插入操作,容易出现主键冲突,因为脚本会在每次单测执行时都执行

@Sql可以指定脚本的执行时机,如在单测方法执行前或执行后,通过executionPhase参数控制

在单测时,可能某些单测只能依赖MySQL,可以通过Spring的Profile功能来实现默认使用H2,但可以通过注解的方式来显示给某些单测指使用MYSQL数据源

1.  <?xml version="1.0" encoding="UTF-8"?>
2.  <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jdbc="http://www.springframework.org/schema/jdbc"
3.      xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
4.              http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
5.              http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd
6.              http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
7.              http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd">
8.   
9.      <beans profile="H2">
10.                    <jdbc:embedded-database id="dataSource" generate-name="true" type="H2">
11.                        <jdbc:script location="classpath:/sql/test_schema.sql" />
12.                    </jdbc:embedded-database>
13.                </beans>
14.             
15.                <beans profile="MYSQL">
16.                    <bean name="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
17.                        <property name="url" value="${db.url}" />
18.                        <property name="username" value="${db.username}" />
19.                        <property name="password" value="${db.password}" />
20.                        <!-- mysql jdbc 6.0driver改包名了 -->
21.                        <property name="driverClassName" value="${db.driver.class}" />
22.                        <property name="initialSize" value="1" />
23.                        <property name="maxActive" value="50" />
24.                        <property name="minIdle" value="1" />
25.                        <property name="maxWait" value="60000" />
26.                        <property name="testOnBorrow" value="false" />
27.                        <property name="testOnReturn" value="false" />
28.                        <property name="testWhileIdle" value="true" />
29.                        <property name="timeBetweenEvictionRunsMillis" value="60000" />
30.                        <property name="minEvictableIdleTimeMillis" value="25200000" />
31.                        <property name="removeAbandoned" value="true" />
32.                        <property name="removeAbandonedTimeout" value="1800" />
33.                        <property name="logAbandoned" value="true" />
34.                        <property name="filters" value="mergeStat" />
35.                    </bean>
36.                </beans>
37.             
38.            </beans>
39.            @RunWith(SpringRunner.class)
40.            @ContextConfiguration(locations = "classpath:spring-test-root.xml")
41.            @ActiveProfiles(profiles = "H2", inheritProfiles = false)
42.            public class BaseTest {
43.             
44.                @Rule
45.                public MockitoRule rule = MockitoJUnit.rule();
46.             
47.            }
48.             
49.            @ActiveProfiles(profiles = "MYSQL", inheritProfiles = false)
50.            public class BTestClass extends BaseTest {
51.             
52.                @Resource
53.                private TestMapper mapper;
54.             
55.                @Test
56.                @Sql("BTestClass_testAAA.sql")
57.                public void testAAA() {
58.                    List<TestModel> all = mapper.getAll();
59.             
60.                    Assert.isTrue(all != null && all.size() == 1);
61.                }
62.             
63.            }
  •  

可以通过Spring元注解功能来使代码更语义化一些

1.  /**
2.   * 选择使用H2数据库还是使用Mysql数据库, 底层使用的是SpringProfile功能
3.   * 
4.   * 可以通过
5.   * 
6.   * @see spring-test-datasource.xml
7.   * @author tudesheng
8.   * @since 2016913下午7:02:38
9.   *
10.             */
11.            @Documented
12.            @Inherited
13.            @Retention(RetentionPolicy.RUNTIME)
14.            @Target(ElementType.TYPE)
15.            @ActiveProfiles(inheritProfiles = false)
16.            public @interface DBSelecter {
17.             
18.                /**
19.                 * profiles, 取值只能是DBTypes.H2, 或者是DBTypes.MYSQL, 默认DBTypes.H2
20.                 */
21.                @AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
22.                String[] value() default { DBTypes.H2 };
23.             
24.                /**
25.                 * profiles, 取值只能是DBTypes.H2, 或者是DBTypes.MYSQL, 默认DBTypes.H2
26.                 */
27.                @AliasFor(annotation = ActiveProfiles.class, attribute = "profiles")
28.                String[] type() default { DBTypes.H2 };
29.             
30.            }
31.             
32.            @RunWith(SpringRunner.class)
33.            @ContextConfiguration(locations = "classpath:spring-test-root.xml")
34.            @DBSelecter(DBTypes.H2)
35.            public class BaseTest {
36.             
37.                @Rule
38.                public MockitoRule rule = MockitoJUnit.rule();
39.             
40.            }
41.             
42.            @DBSelecter(DBTypes.MYSQL)
43.            public class BTestClass extends BaseTest {
44.             
45.                @Resource
46.                private TestMapper mapper;
47.             
48.                @Test
49.                @Sql("BTestClass_testAAA.sql")
50.                public void testAAA() {
51.                    List<TestModel> all = mapper.getAll();
52.             
53.                    Assert.isTrue(all != null && all.size() == 1);
54.                }
55.             
56.            }
  •  

在使用MYSQL等外部数据库时,单测的执行很容易产生脏数据,可以通过@Rollback注解来标注单测方法执行完后,回滚数据库操作,减少对外部测试数据库的污染

最后,某些单测方法执行后可能会污染Spring的上下文,比如通过反射将某个容器内的Bean的属性给替换调了,可能会对其它的单测造成影响,这个时候,可以通过@DirtiesContext标注该单测方法会污染Spring上下文,需要在单测执行前或执行后重新初始化Spring上下文,慎用,容易增加单测的执行时间

以上是关于单元测试基础知识的主要内容,如果未能解决你的问题,请参考以下文章

junit4单元测试--web项目中模拟登录会话,做全流程测试

单元测试不了解 XCTest 期望的异步 UI 代码?

四则运算单元测试

单元测试很棒,但是

词频统计单元测试

单元测试基础知识