以一个判断用户是否为系统管理员的service方法为例:
// 对应的interface
public interface AuthorityService {
boolean isSystemAdministrator(String user);
}
// 对应的service实现
@Service
public class AuthorityServiceImpl implements AuthorityService {
@Autowired
private AuthorityMapper authorityMapper;
@Override
public boolean isSystemAdministrator(String user) {
// 系统管理员的定义:属于system项目,且角色为SYSTEM
Authority authority = authorityMapper.getByUserAndProject(user, "system");
if (authority != null && Role.SYSTEM.equals(authority.getRole())) {
return true;
}
return false;
}
}
MySQL数据库的访问,是使用Mapper接口 + Mapper.xml实现的
// 对应的Mapper.xml实现,这里省略
public interface AuthorityMapper extends BaseMapper<Authority> {
Authority getByUserAndProject(String user, String project);
}
单元测试的逻辑(这里暂不考虑代码执行存在异常的情况):
SYSTEM
,返回true;否则,返回false整体的单元测试代码如下:
// 这里一系列的注解,都是为了能在单元测试时启动整个服务,比如连接数据库、访问配置中心等
// 这里主要是为了实现数据库的连接
@RunWith(SpringRunner.class)
@SpringBootTest(classes = PlatformApplication.class)
@DirtiesContext
public class AuthorityServiceImplTest {
private final static Logger logger = LoggerFactory.getLogger(AuthorityServiceImplTest.class);
@Rule
public MockitoRule rule = MockitoJUnit.rule();
@Autowired
AuthorityService authorityService;
@Test
@Transactional // 单元测试结束后,会清理数据库中的记录
public void testIsSystemAdministrator() {
// 插入一条权限记录
String user = RandomStringUtils.random(8, false, true);
Authority authority = new Authority(user, Role.SYSTEM, "11120066", "system_test", "2020-10-26 12:24:45", true);
authorityService.add(authority);
// 资源组不是system,因此直接返回false
Assert.assertFalse(authorityService.isSystemAdministrator(user));
// 插入一条权限记录
user = RandomStringUtils.random(8, false, true);
authority = new Authority(user, Role.SYSTEM, "11120066", "system", "2020-10-26 12:24:45", true);
authorityService.add(authority);
// 项目为system且角色为SYSTEM,是系统管管理员,返回true
Assert.assertTrue(authorityService.isSystemAdministrator(user));
// 插入一条权限记录
user = RandomStringUtils.random(8, false, true);
authority = new Authority(user, Role.NORMAL, "11120066", "system", "2020-10-26 12:24:45", true);
authorityService.add(authority);
// 项目为system,但角色不是SYSTEM,不是系统管理员,返回false
Assert.assertFalse(authorityService.isSystemAdministrator(user));
}
}
主要使用到了如下的jar包依赖:
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-webartifactId>
<version>4.3.22.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-testartifactId>
<version>1.5.19.RELEASEversion>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-testartifactId>
<version>4.3.22.RELEASEversion>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>4.3.22.RELEASEversion>
dependency>
@Transactional
注解在单元测试执行完后清理数据
- This unit test is slow, because you need to start a database in order to get data from DAO.
- This unit test is not isolated, it always depends on external resources like database.
- This unit test can’t ensures the test condition is always the same, the data in the database may vary in time.
- It’s too much work to test a simple method, cause developers skipping the test.
In summary, what we want is a simple, fast, and reliable unit test instead of a potentially complex, slow, and flaky test!
使用Mockit实现DAO层接口的模拟,使其无需访问数据库,就能在在特定入参条件下,返回特定的值
这里只展示测试逻辑中的一种,用户是系统管理员的情况
import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class AuthorityServiceTest {
@Test
public void testIsSystemAdministrator() {
// 1. 构建权限记录,作为DAO层方法的、指定的返回值
Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
// 2. 使用Mockit模拟出一个mapper
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 3. 设置访问方法的返回值
when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(authority);
// 4. 创建service,传入mock出来的mapper
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
// 5. 访问方法,将调用AuthorityMapper的getByUserAndProject()方法,返回指定的authority
// 从而使得isSystemAdministrator()判断的结果为true
Assert.assertTrue(service.isSystemAdministrator("sunrise"));
}
}
需要在pom.xml文件中,添加Mockit的maven依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.17.0</version>
<scope>test</scope>
</dependency>
JUnit和Mockit框架中,很多方法都是静态方法,为了使得的代码简洁,通过import static进行引入。
为了节省篇幅,后续代码示例,将不再给出引用static方法的import语句
@Transactional
的使用)src/main/test
,生产代码存放在src/main/java
Assert.assertTrue()
判断boolean类型,使用Assert.assertEquals()
判断数值、字符串或其他自定义类mock
)外部服务,只关注被测代码在不同场景下的执行逻辑mock的重要性
test doubles
delete()
方法没有返回值,单元测试时,只需要关注该替身的delete()方法否被调用所谓的Plain Mockit,就是使用Mockit的 静态 方法mock()
创建一个替身
例如,前面创建AuthorityMapper替身时,就是使用的plain Mockit
AuthorityMapper mapper = Mockit.mock(AuthorityMapper.class);
下面的代码展示了,如何使用@Mock
注解创建替身
注意: 使用@Mock
注解只是标识这是一个替身,还需要通过MockitoAnnotations.initMocks()
初始化替身
// 必须添加该注解,否则替身无法初始化,使用时将抛出NullPointerException
@RunWith(MockitoJUnitRunner.class)
public class MyAuthorityServiceTest {
@Mock
private AuthorityMapper mock; // 只是添加了@Mock注解,并没有创建替身
@Before // 替身的初始化,需要在测试类的启动方法中完成,因此使用@Before标识setUp()方法
public void setUp() {
// initMocks()负责为当前类中,添加了@Mock注解的字段或入参创建替身
MockitoAnnotations.initMocks(this);
}
@Test
public void testIsSystemAdministrator() {
Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
when(mock.getByUserAndProject("sunrise", "system")).thenReturn(authority);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mock);
Assert.assertTrue(service.isSystemAdministrator("sunrise"));
}
}
注意: @RunWith(MockitoJUnitRunner.class)
是替身成功初始化的关键
感谢Stack Overflow的提问:mock instance is null after @Mock annotation
使用@Mock
注解仍然无法实现替身的自动创建,JUnit Jupiter的MockitoExtension
可以实现
具体代码如下:
@RunWith(MockitoJUnitRunner.class)
@ExtendWith(MockitoExtension.class)
public class MyAuthorityServiceTest {
@Mock
private AuthorityMapper mock;
@Test
public void testIsSystemAdministrator() {
Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
when(mock.getByUserAndProject("sunrise", "system")).thenReturn(authority);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mock);
Assert.assertTrue(service.isSystemAdministrator("sunrise"));
}
}
注意: 需要将JUnit从4升级到5,否则单元测试会报错java.lang.NoClassDefFoundError: org/junit/jupiter/api/extension/ScriptEvaluationException
<dependency>
<groupId>org.mockitogroupId>
<artifactId>mockito-junit-jupiterartifactId>
<version>2.17.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.jupitergroupId>
<artifactId>junit-jupiter-engineartifactId>
<version>5.2.0version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.junit.platformgroupId>
<artifactId>junit-platform-runnerartifactId>
<version>1.2.0version>
<scope>testscope>
dependency>
when().thenReturn()
:让替身返回指定值when().thenReturn()
,定义了替身在getByUserAndProject()
方法的行为:以入参sunrise、system调用getByUserAndProject()方法,则返回指定的权限记录when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(authority);
when(x).thenReturn(y)
的含义:When the x method is called then return yoverwrite
替身行为的情况下,只要getByUserAndProject()方法的入参为sunrise、system,多次调用该方法都将返回同样的权限记录有时,我们希望连续调用替身的方法时,替身能展现出不同的行为(如返回不同的值)
这时,如果使用多个when().thenReturn();
,代码将显得非常冗余
希望能像Builder模式设置属性一样,能一次定义多个返回值 —— Mockit支持该特性
@Test
public void isSystemAdministratorTest() {
Authority authority1 = new Authority("sunrise", Role.SYSTEM, "system", "jack");
Authority authority2 = new Authority("sunrise", Role.NORMAL, "system", "jack");
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 定义不同的返回值,以便在连续方法调用中返回不同的值
when(mapper.getByUserAndProject("sunrise", "system"))
.thenReturn(authority1)
.thenReturn(authority2)
.thenReturn(null);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
// 测试sunrise为系统管理员的情况
assertTrue(service.isSystemAdministrator("sunrise"));
// 测试sunrise权限不足,不是系统管理员的情况
assertFalse(service.isSystemAdministrator("sunrise"));
// 测试sunrise的信息不存在的情况
assertFalse(service.isSystemAdministrator("sunrise"));
// 注意: 后续的调用,替身都将返回null
assertFalse(service.isSystemAdministrator("sunrise"));
}
除了上述这种多次thenReturn()
的写法,还可以精简为一个thenReturn()
when(mapper.getByUserAndResource("sunrise", "system"))
.thenReturn(authority1, authority2, null);
上面的代码示例,调用替身方法时,都使用了符合要求的入参,因此都能返回thenReturn()
中指定的值
如果方法入参不满足when()
中规定的条件,则将根据方法的返回值类型,返回一个默认值
int/Integer
,将返回0boolean/Boolean
,将返回false下面的代码,展示了替身方法的默认返回值
@Test
public void defaultReturnValueTest() {
Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 返回值为Authority类型
when(mapper.getByUserAndResource("sunrise", "system")).thenReturn(authority);
// 入参不符合要求,返回默认值null
assertNull(mapper.getByUserAndResource("jack", "test-project"));
// insert()方法的返回值为int类型,即affected rows
when(mapper.insert(authority)).thenReturn(1);
// 入参不符合要求,返回默认值0
assertEquals(0, mapper.insert(new Authority()));
}
在进行单元测试时,我们不关心方法入参的具体值,只要类型符合要求,替身都能返回相同的值
例如,DAO层的insert操作,无论插入什么样的记录,默认成功插入,返回affected rows为1
这时候可以使用ArgumentMatchers
提供的各种静态any()/anyX()
方法
any(Class type)
:匹配任何指定Class类型的入参,不包括nullanyObject()
:允许任何对象作为入参,包括nullanyByte()
、anyChar()
、anyInt()
等anyString()
:允许任何String类型的入参,不包括null对service的insert()方法进行单元测试,使用any(Authority.class)
表示只要是Authority类型的入参都符合要求
// 单元测试的代码
@Test
public void insertTest() {
Authority authority1 = new Authority("sunrise", Role.SYSTEM, "system", "jack");
// NewAuthority extends Authority
Authority authority2 = new NewAuthority("john", Role.SYSTEM, "test_project_1", "jack");
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 只要插入Authority,都统一返回1,表示成功插入数据
when(mapper.insert(any(Authority.class))).thenReturn(1);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
// 成功插入authority1
assertTrue(service.insert(authority1));
// 成功authority2
assertTrue(service.insert(authority2));
// 插入null,不是Authority类型,将返回默认值0,导致insert操作失败
assertFalse(service.insert(null));
}
// service.insert()方法的实现
@Override
public boolean insert(Authority authority) {
int row = authorityMapper.insert(authority);
if (row == 1) {
return true;
}
return false;
}
单元测试时,除了希望替身能返回指定值,还希望替身能抛出异常,以测试目标方法能否处理异常
这时,可以使用when().thenThrow()
让替身抛出异常
下面的代码示例,让替身的insert()方法抛出异常,最终该异常将被service层的insert()方法上抛
@Test(expected = RuntimeException.class)
public void insertTest() {
Authority authority = new Authority("lucy", Role.SYSTEM, "system", "jack");
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 插入的权限记录,user字段过长,触发异常
when(mapper.insert(authority)).thenThrow(new RuntimeException("Unexpected error when insert data"));
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
// service的insert()方法,没有进行异常处理,DAO层的异常将上抛
service.insert(authority);
}
doThrow()
AuthorityMapper有一个delete方法,定义如下:
void deleteData(long id);
service的delete方法定义如下,调用了AuthorityMapper的delete方法:
@Override
public void delete(long userId) {
authorityMapper.deleteData(userId);
}
使用when().thenThrow()
让mapper替身在执行delete方法时抛出异常,此时发现IDE提示代码编写错误
查看when()方法的源码,发现它是一个泛型方法,会返回一个包含被调方法的OngoingStubbing
@CheckReturnValue
public static <T> OngoingStubbing<T> when(T methodCall) {
return MOCKITO_CORE.when(methodCall);
}
对于返回值为void的方法,是不能使用when()定义调用条件的
对于返回值为void的方法,可以使用doThrow(exception).when(testDoubles).methodCall()
来定义抛出异常
@Test(expected = RuntimeException.class)
public void deleteTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
// deleteData()返回值为void,使用doThrow()定义异常
doThrow(new RuntimeException("Unexpected error when delete data")).when(mapper).deleteData(anyLong());
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
// service的delete()方法,没有进行异常处理,异常将上抛
service.delete(1024);
}
when().then...
除了支持定义多个返回值,还允许定义多个异常,甚至还能返回值和异常一起定义
一次定义多个异常:
@Test
public void isSystemAdministratorTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 定义多个异常
when(mapper.getByUserAndProject("sunrise", "system"))
.thenThrow(new RuntimeException("Unexpected error"))
.thenThrow( new SecurityException("Can not modify the database"));
// 等同于如下语句
/* when(mapper.getByUserAndResource("sunrise", "system"))
.thenThrow(new RuntimeException("Unexpected error"), new SecurityException("Can not modify the database")); */
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
try {
service.isSystemAdministrator("sunrise");
} catch (RuntimeException exception) {
assertEquals("Unexpected error", exception.getMessage());
}
try {
service.isSystemAdministrator("sunrise");
} catch (SecurityException exception) {
assertEquals("Can not modify the database", exception.getMessage());
}
}
返回值和异常一起定义
@Test
public void isSystemAdministratorTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 返回值和异常一起定义, 无法精简到一个语句中
when(mapper.getByUserAndProject("sunrise", "system"))
.thenReturn(null)
.thenThrow( new SecurityException("Can not modify the database"));
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
assertFalse(service.isSystem("sunrise"));
try {
service.isSystem("sunrise");
} catch (SecurityException exception) {
assertEquals("Can not modify the database", exception.getMessage());
}
}
一次偶然的机会,将异常和返回值分开定义了,竟然运行报错
// 异常和返回值的定义分开,其余代码与上一个示例保持一致
when(mapper.getByUserAndProject("sunrise", "system"))
.thenThrow(new SecurityException("Can not modify the database"));
when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(null);
后来仔细阅读了Mockit的代码注释,发现需要使用doReturn().when()
替代when().thenReturn()
,已覆盖之前定义的的异常行为
@Test
public void isSystemAdministratorTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
// 返回值和异常分开定义
when(mapper.getByUserAndResource("sunrise", "system"))
.thenThrow(new SecurityException("Can not modify the database"));
doReturn(null).when(mapper).getByUserAndProject("sunrise", "system");
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
try {
service.isSystem("sunrise");
} catch (SecurityException exception) {
logger.info(exception.getMessage()); // 稍微有点改动,直接打印异常
}
assertFalse(service.isSystem("sunrise"));
}
运行代码,发现未打印异常信息。
原因: 使用doReturn().when()
替代when().thenReturn()
,成功覆盖了之前定义的异常行为,使得后续调用不会再抛出异常
注意:
when().then...
一次定义多个行为时,异常和返回特定值的行为,是按照顺序出现的when().then...
定义替身行为,后面的行为将覆盖前面的行为doReturn().when()
,而非when().thenReturn()
对于返回值为void的方法,定义替身行为时,我们更加关心该方法是否被成功调用
因为返回值为void,无需通过thenReturn()
或doReturn()
让其返回特定值
这时,可以使用verify()
验证void方法的调用次数。默认为一次,还可以通过times()
指定调用次数
@Test
public void deleteTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
service.delete(1024);
// deleteData()返回值为void,使用verify()校验调用次数,默认为一次
verify(mapper).deleteData(1024);
// 还可以指定调用次数
verify(mapper, times(0)).deleteData(256);
// 等价于下面的调用
verify(mapper, never()).deleteData(256);
}
注意:verify()不仅局限于void方法,非void方法也可以使用
有时,方法的调用次数不是确定,我们只知道调用次数的上限或者下限
这时,可以使用atMost()
、atLeast()
进行限制
@Test
public void deleteTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
AuthorityServiceImpl service = new AuthorityServiceImpl();
service.setAuthorityMapper(mapper);
service.delete(1024);
// 调用替身的deleteData(1024)方法至多一次
verify(mapper, atMost(1)).deleteData(1024);
// 等价于下面的语句
verify(mapper, atMostOnce()).deleteData(1024);
service.delete(1024);
// 调用次数是个累积值,此时deleteData(1024)已被调用2次
verify(mapper, atLeast(2)).deleteData(1024);
// 至少一次的简写
verify(mapper, atLeastOnce()).deleteData(1024);
}
除了调用指定的方法,我们希望替身的其他方法为被调用,这时可以使用verifyNoMoreInteractions()
或者verifyZeroInteractions()
@Test
public void verifyTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
mapper.deleteData(1024);
Authority authority = new Authority("sunrise", Role.ADMIN, "test-project", "jack");
mapper.insert(authority);
// 校验被调用过的方法
verify(mapper).deleteData(1024);
verify(mapper).insert(authority);
// 除了被校验的方法,没有其他方法被调用
verifyNoMoreInteractions(mapper);
// 等价于下面的方法
verifyZeroInteractions(mapper);
}
doReturn()
大多数情况下,doReturn().when()
与when().thenReturn()
二者等价,都是让替身返回特定值
且when().thenReturn()
更直白易懂,因此建议使用when().thenReturn()
但在一些特殊情况下,必须使用doReturn().when()
替代when().thenReturn()
情况1: doReturn().when()
覆盖前面的异常定义,详情见4.2.3
情况2: 使用spy()监视真实对象,在spy上调用真实方法会产生副作用时,需要使用doReturn().when()
例如,使用spy()监视list对象,并在spy上调用get()方法,获取list中的元素
如果使用when().thenReturn()
,规定get()方法的返回值,可能会引发错误
@Test
public void spyTest() {
List<Integer> list = new ArrayList<>();
List<Integer> spy = spy(list);
// 定义spy的行为
when(spy.get(0)).thenReturn(10);
// 调用spy
assertEquals(Integer.valueOf(10), spy.get(0));
}
执行上面的代码,发现在使用when().thenReturn()
定义行为时,就出现了IndexOutOfBoundsException
错误原因:
when().thenReturn()
定义spy行为时,将调用被监视对象的真实方法get(0)
访问其元素时,将触发IndexOutOfBoundsException
正确做法: 使用doReturn().when()
替代when().thenReturn()
@Test
public void spyTest() {
List<Integer> list = new ArrayList<>();
List<Integer> spy = spy(list);
// 定义spy的行为
doReturn(10).when(spy).get(0);
// 调用spy
assertEquals(Integer.valueOf(10), spy.get(0));
}
thenAnswer()
或让替身行为更复杂thenReturn()
或doReturn()
,只能让替身返回特定的值
我们希望替身返回的值与方法入参有关系,或者随着方法调用而变化
例如,DAO层的insert()
方法,返回自增的主键id的值;每次调用,返回的值应该有所变化
这时,可以使用thenAnswer()
让替身行为更复杂
@Test
public void insertTest() {
Authority authority = new Authority("lucy", Role.SYSTEM, "system", "jack");
AuthorityMapper mapper = mock(AuthorityMapper.class);
AtomicInteger id = new AtomicInteger();
// insert方法,返回递增的id主键值
when(mapper.insert(authority)).thenAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
Authority input = invocation.getArgument(0); // 获取入参
System.out.println("成功插入数据: " + input); // 打印入参
return id.incrementAndGet(); // 返回自增的主键id的值
}
});
// 第一次调用,主键值为1
assertEquals(1, mapper.insert(authority));
}
对于返回值为void的替身方法,可以使用doAnswer()
丰富替身行为
@Test(expected = IllegalArgumentException.class)
public void deleteTest() {
AuthorityMapper mapper = mock(AuthorityMapper.class);
// lambda表达式实现Answer接口,当id不是1024时,抛出异常;否则,返回null
doAnswer(invocation -> {
Long id = invocation.getArgument(0);
if (id != 1024L) {
throw new IllegalArgumentException("不存在id为" + id + "的记录");
}
System.out.println("成功删除id为" + id + "的记录");
return null;
}).when(mapper).deleteData(anyLong()); // 输入值为任意类型
mapper.deleteData(1024L);
mapper.deleteData(128L); // 触发异常
}
when().thenX()
定义替身行为,特殊情况需要转为使用对应的doX().when()