以一个判断用户是否为系统管理员的service方法为例:
// 对应的interface
public interface AuthorityService {boolean isSystemAdministrator(String user);
}// 对应的service实现
@Service
public class AuthorityServiceImpl implements AuthorityService {private final static Logger logger = LoggerFactory.getLogger(AuthorityServiceImpl.class);@Autowiredprivate AuthorityMapper authorityMapper;@Overridepublic boolean isSystemAdministrator(String user) {// 系统管理员的定义:属于system项目,且角色为SYSTEMAuthority 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 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);@Rulepublic MockitoRule rule = MockitoJUnit.rule();@AutowiredAuthorityService 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,因此直接返回falseAssert.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,是系统管管理员,返回trueAssert.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,不是系统管理员,返回falseAssert.assertFalse(authorityService.isSystemAdministrator(user));}
}
主要使用到了如下的jar包依赖:
junit junit 4.12
org.springframework spring-web 4.3.22.RELEASE
org.springframework.boot spring-boot-test 1.5.19.RELEASE test
org.springframework spring-test 4.3.22.RELEASE test
org.springframework spring-jdbc 4.3.22.RELEASE
@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!
import org.junit.Assert;
import org.junit.Test;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;public class AuthorityServiceTest {@Testpublic void testIsSystemAdministrator() {// 1. 构建权限记录,作为DAO层方法的、指定的返回值Authority authority = new Authority("sunrise", Role.SYSTEM, "system", "jack");// 2. 使用Mockit模拟出一个mapperAuthorityMapper mapper = mock(AuthorityMapper.class);// 3. 设置访问方法的返回值when(mapper.getByUserAndProject("sunrise", "system")).thenReturn(authority);// 4. 创建service,传入mock出来的mapperAuthorityServiceImpl service = new AuthorityServiceImpl();service.setAuthorityMapper(mapper);// 5. 访问方法,将调用AuthorityMapper的getByUserAndProject()方法,返回指定的authority// 从而使得isSystemAdministrator()判断的结果为trueAssert.assertTrue(service.isSystemAdministrator("sunrise"));}
}
@Transactional的使用)src/main/test,生产代码存放在src/main/javaAssert.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 {@Mockprivate AuthorityMapper mock; // 只是添加了@Mock注解,并没有创建替身@Before // 替身的初始化,需要在测试类的启动方法中完成,因此使用@Before标识setUp()方法public void setUp() {// initMocks()负责为当前类中,添加了@Mock注解的字段或入参创建替身MockitoAnnotations.initMocks(this); }@Testpublic 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 {@Mockprivate AuthorityMapper mock;@Testpublic 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
org.mockito mockito-junit-jupiter 2.17.0 test
org.junit.jupiter junit-jupiter-engine 5.2.0 test
org.junit.platform junit-platform-runner 1.2.0 test
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"));// 注意: 后续的调用,替身都将返回nullassertFalse(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);// 入参不符合要求,返回默认值nullassertNull(mapper.getByUserAndResource("jack", "test-project"));// insert()方法的返回值为int类型,即affected rowswhen(mapper.insert(authority)).thenReturn(1);// 入参不符合要求,返回默认值0assertEquals(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 AuthorityAuthority 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);// 成功插入authority1assertTrue(service.insert(authority1));// 成功authority2assertTrue(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 OngoingStubbing 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);
出错的代码行号,竟然是when().thenReturn()语句

后来仔细阅读了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 list = new ArrayList<>();List spy = spy(list);// 定义spy的行为when(spy.get(0)).thenReturn(10);// 调用spyassertEquals(Integer.valueOf(10), spy.get(0));
}
执行上面的代码,发现在使用when().thenReturn()定义行为时,就出现了IndexOutOfBoundsException

错误原因:
when().thenReturn()定义spy行为时,将调用被监视对象的真实方法get(0)访问其元素时,将触发IndexOutOfBoundsException正确做法: 使用doReturn().when()替代when().thenReturn()
@Test
public void spyTest() {List list = new ArrayList<>();List spy = spy(list);// 定义spy的行为doReturn(10).when(spy).get(0);// 调用spyassertEquals(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() {@Overridepublic Integer answer(InvocationOnMock invocation) throws Throwable {Authority input = invocation.getArgument(0); // 获取入参System.out.println("成功插入数据: " + input); // 打印入参return id.incrementAndGet(); // 返回自增的主键id的值}});// 第一次调用,主键值为1assertEquals(1, mapper.insert(authority));
}
对于返回值为void的替身方法,可以使用doAnswer()丰富替身行为
@Test(expected = IllegalArgumentException.class)
public void deleteTest() {AuthorityMapper mapper = mock(AuthorityMapper.class);// lambda表达式实现Answer接口,当id不是1024时,抛出异常;否则,返回nulldoAnswer(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()