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;
- 微服务场景下特别常用,因为任何外部依赖的存在都会极大的限制测试用例的可迁移性和稳定性;
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>