几乎所有测试工具都可以和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[]
为什么要使用mock
:
TODO: mockito文档待补充
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");
}
}
传统的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));
}
}
用于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());
}
}
主要介绍测试用例参数化,可以做到测试数据外置,用不同用例反复执行某一个测试,并且突破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.jupitergroupId>
<artifactId>junit-jupiter-apiartifactId>
<version>5.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-engineartifactId>
<version>5.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-launcherartifactId>
<version>1.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-engineartifactId>
<version>1.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-commonsartifactId>
<version>1.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-paramsartifactId>
<version>5.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
<version>5.5.1version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-surefire-providerartifactId>
<version>1.3.2version>
<scope>testscope>
dependency>
...
<plugin>
<artifactId>maven-surefire-pluginartifactId>
<dependencies>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-surefire-providerartifactId>
<version>1.3.2version>
dependency>
dependencies>
plugin>
JUnit参数化测试的讨论