【单元测试】不同类型的单元测试及其使用场景

JUnit

几乎所有测试工具都可以和JUnit集成,以增强JUnit的功能。
纯的Junit只能测试较为简单的方法,比如工具类、static方法、上下文无关的方法等。
简单的JUnit测试不依赖任何外部资源,不需要加载任何上下文,直接开测,比如一个格式化文件路径的方法:

package github.clyoudu.util;
 
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
 
import java.util.regex.Matcher;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/7/29 10:01
 * @description FileUtil
 */
@Slf4j
public class FileUtil {
 
    private static final String DOUBLE_SLASH = "//";
    private static final String DOUBLE_BACK_SLASH = "\\\\";
    private static final String SLASH = "/";
    private static final String BACK_SLASH = "\\";
 
    private FileUtil() {
    }
 
    public static String formatFilePath(String filePath) {
        if (StringUtils.isBlank(filePath)) {
            return filePath;
        }
 
        while (filePath.contains(DOUBLE_SLASH) || filePath.contains(DOUBLE_BACK_SLASH)) {
            filePath = filePath.replaceAll(DOUBLE_SLASH, SLASH).replaceAll(Matcher.quoteReplacement(DOUBLE_BACK_SLASH), Matcher.quoteReplacement(BACK_SLASH));
        }
 
        return filePath;
    }
 
}

JUnit示例测试代码如下:

package github.clyoudu.util;
 
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
 
import java.util.Collection;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/8/6 20:23
 * @description FileUtil1Test
 */
@RunWith(Parameterized.class)
public class FileUtil1Test {
 
    private String path;
    private String expected;
 
    public FileUtil1Test(String path, String expected) {
        this.path = path;
        this.expected = expected;
    }
 
    @Parameterized.Parameters
    public static Collection<Object[]> getTestData() {
        return ArraysUtil.asList(new Object[][]{
                {"///a//b//c/d/e.txt", "/a/b/c/d/e.txt"},
                {"C:\\a\\\\b\\\\\\\\\\c\\d\\\\\\e.txt", "C:\\a\\b\\c\\d\\e.txt"}
        });
    }
 
    @Test
    public void test() {
        Assert.assertEquals(expected, FileUtil.formatFilePath(path));
    }
 
}

JUnit有很多注解,在一个JUnit测试中发挥不同作用,JUnit的生命周期可以用如下代码测试:

package github.clyoudu.util;
 
import lombok.extern.slf4j.Slf4j;
import org.junit.*;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/8/7 21:03
 * @description JUnitLifeCycleTest
 */
@Slf4j
public class JUnitLifeCycleTest {
 
    public JUnitLifeCycleTest() {
        log.info("constructor");
    }
 
    @BeforeClass
    public static void beforeClass1() {
        log.info("before class 1");
    }
 
    @BeforeClass
    public static void beforeClass2() {
        log.info("before class 2");
    }
 
    @Before
    public void before1() {
        log.info("before 1");
    }
 
    @Before
    public void before2() {
        log.info("before 2");
    }
 
    @Test
    public void test1() {
        log.info("test 1");
    }
 
    @Test
    public void test2() {
        log.info("test 2");
    }
 
    @After
    public void after1() {
        log.info("after 1");
    }
 
    @After
    public void after2() {
        log.info("after 2");
    }
 
    @AfterClass
    public static void afterClass1() {
        log.info("after class 1");
    }
 
    @AfterClass
    public static void afterClass2() {
        log.info("after class 2");
    }
     
/**
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:27] before class 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:22] before class 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:17] constructor
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:37] before 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:32] before 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:42] test 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:52] after 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:57] after 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:17] constructor
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:37] before 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:32] before 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:47] test 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:52] after 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:57] after 2
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:62] after class 1
  * [2019-08-07 21:09:03] [main] [INFO] [github.clyoudu.util.JUnitLifeCycleTest:67] after class 2
  */
 
}

所以JUnit测试各个注解的先后执行顺序为:@BeforeClass[] -> Constructor -> @Before[] -> @Test[1] -> @After[] -> ... -> @Before[] -> @Test[n] -> @After[] -> @AfterClass[]

Mockito

为什么要使用mock

  • 创建所需的DB数据可能需要很长时间,或者编写满足所有外部依赖的测试可能很复杂,复杂到不值得编写,Mock模拟内部或外部依赖可以帮助我们解决这些问题,比如非常复杂的关联查询,但测试本方法并不关注查询本身是否正确;
  • 调用第三方API接口,测试很慢,比如接口真实上传一个文件或其它耗时操作;
    对象的结果不确定,如获取当前时间、生成任意随机数,每次得到的结果都不一样,无法符合我们的预期;
  • 实现这个接口的对象不存在,比如基于接口契约的开发,或者开发未完成的HTTP接口和FeignClient;
  • 微服务场景下特别常用,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性;

TODO: mockito文档待补充

PowerMock

powermock可以mock静态方法、私有方法和final方法,这是mockito不能实现的,因为powermock是直接修改字节码实现方法拦截的,但mockito是基于继承实现的方法拦截,而static/private/final修饰的方法是无法继承的。

那static/private/final的方法为什么还要mock呢?因为某些static方法读取了真实的外部资源,比如读取文件、打开socket端口、连接数据库等,但我们测试的时候不应该使用真实的使用外部资源,甚至根本获取不到真实的外部资源,而private/final方法也可能使用了外部资源,或者私有方法特别耗时,这些情况下就可以使用powermock

一个例子:

package github.clyoudu.util;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

/**
 * Created by IntelliJ IDEA
 *
 * @author chenlei
 * @date 2019/8/11
 * @time 20:19
 * @desc FileUtil3Test
 */
@RunWith(PowerMockRunner.class)
@PrepareForTest({FileUtil.class})
public class FileUtil3Test {

    @Before
    public void setup() {
        PowerMockito.mockStatic(FileUtil.class);
    }

    @Test
    public void test() {
        PowerMockito.when(FileUtil.formatFilePath(Mockito.anyString())).thenReturn("test result");
        Assert.assertEquals(FileUtil.formatFilePath("123123"), "test result");
    }

}

SpringJunit

传统的spring项目如果需要加载spring context,让一些component从spring容器中获取,那么只需要使用相应的注解即可:

package github.clyoudu.util;
 
import github.clyoudu.repository.dao.MysqlServiceDao;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/8/7 22:03
 * @description SpringJunitTest
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:application-context.xml")
public class SpringJunitTest {
 
    @Autowired
    private MysqlServiceDao mysqlServiceDao;
 
    @Test
    public void test() {
        Assert.assertEquals(null, mysqlServiceDao.selectByDbServiceId(null));
    }
 
}

SpringbootTest

用于springboot项目中和spring test类似,使用不同的注解:

package github.clyoudu.repository.dao;
 
import github.clyoudu.repository.jooq.dbaas.tables.records.DbIdentityRecord;
import github.clyoudu.utils.security.SecurityUtils;
import org.junit.Assert;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import java.util.List;
import java.util.UUID;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/7/31 17:10
 * @description DbIdentityDaoTest
 */
@SpringBootTest
@RunWith(SpringRunner.class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DbIdentityDaoTest {
 
    @Autowired
    private DbIdentityDao dbIdentityDao;
 
    private String dbPk = "test-db-pk";
 
    @Test
    public void test01Insert() {
        DbIdentityRecord record = new DbIdentityRecord();
        record.setDbPk(dbPk);
        record.setDbType("mysql");
        record.setIdentityId(UUID.randomUUID().toString());
        record.setStatus(1);
        record.setUsername("root");
        record.setPassword(SecurityUtils.encode("root"));
        record.setUserType(3);
 
        int affectedRows = dbIdentityDao.insert(record);
        Assert.assertEquals(1, affectedRows);
    }
 
    @Test
    public void test02SelectByDbPk() {
        List<DbIdentityRecord> list = dbIdentityDao.selectByDbPk(dbPk);
        Assert.assertEquals(1, list.size());
        Assert.assertEquals(dbPk, list.get(0).getDbPk());
    }
 
}

SpringbootTest + JUnit 5

主要介绍测试用例参数化,可以做到测试数据外置,用不同用例反复执行某一个测试,并且突破junit 4的限制。
一些参考资料:
JUnit 5 User Guide
JUnit 5 – Parameterized Tests

package github.clyoudu.repository.dao;
 
import github.clyoudu.repository.jooq.dbaas.tables.records.SysUsersRecord;
import org.junit.Assert;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.EmptySource;
import org.junit.jupiter.params.provider.NullSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
 
/**
 * Create by IntelliJ IDEA
 *
 * @author chenlei
 * @dateTime 2019/8/7 23:18
 * @description JUnit5Test
 */
@ExtendWith(SpringExtension.class)
@SpringBootTest
public class JUnit5Test {
 
    @Autowired
    private SysUserDao sysUserDao;
 
    @ParameterizedTest
    @ValueSource(strings = {"1adbe15530c345a4a9c9f6ac91291457"})
    void testSuccess(String id) {
        SysUsersRecord record = sysUserDao.getUser(id);
        Assert.assertEquals("test", record.getUsername());
    }
 
    @ParameterizedTest
    @EmptySource
    void testEmptyId(String id) {
        Assert.assertEquals(null, sysUserDao.getUser(id));
    }
 
    @ParameterizedTest
    @NullSource
    void testNullId(String id) {
        Assert.assertEquals(null, sysUserDao.getUser(id));
    }
 
    @DisplayName("select by service id not exist")
    @ParameterizedTest(name = "select by {0} returns null")
    @CsvSource({"ss","dd","ff","gg","hh","jj","kk","ll","zz","xx","cc","vv","bb"})
    void testIdNotExist(String id) {
        Assert.assertEquals(null, sysUserDao.getUser(id));
    }
 
}

springboot 2.x集成了junit5,但springboot 2.1.3.RELEASE 只支持到Junit 5.3.2,想使用更高版本的特性,比如上面的@NullSource注解,需要指定版本,添加如下依赖:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-engine</artifactId>
    <version>1.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-commons</artifactId>
    <version>1.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <version>5.5.1</version>
    <scope>test</scope>
</dependency>
 
<dependency>
    <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-surefire-provider</artifactId>
    <version>1.3.2</version>
    <scope>test</scope>
</dependency>
 
...
 
<!-- 较低版本IDEA需要添加如下插件 -->
<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <dependencies>
        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-surefire-provider</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
</plugin>

JUnit参数化测试的讨论

JUnit参数化测试的讨论

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值