文章目录
0
所有代码均可在线查看或本地克隆:mockito-shop
环境:
jdk 1.8
maven 3.3.9
sprint boot 2.1.6-RELEASE(junit 4.12 mockito 2.23.4 powemock 2.0.2)
IDEA Intellij IDEA 2017
lombok 1.18.10
参数匹配和返回值Mock
简单匹配
ArgumentMatchers
工具类实现了很多常用的参数匹配器,比如anyString(); anyMap(); any(); any(Class<T>); nullable(Class<T>);eq(); startWith(); contains();notNull()
等,但需要注意的是mockito 1.x和2.x在使用anyXXX()
上是有区别的:
@RunWith(MockitoJUnitRunner.class)
public class SimpleArgumentMatcher {
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
/**
* anyLong() can not match null when using mockito 2.x(works well in mockito 1.x), use nullable(Long.class) instead.
* delete @Ignore annotation for test
*/
@Test
@Ignore
public void testSelectById () {
Mockito.when(userService.selectById(Mockito.anyLong())).thenReturn(new User(0L, "mock-username", "mock-email"));
User user = userController.userInfo(null);
Assert.assertArrayEquals(
new Object[]{0L, "mock-username", "mock-email"},
new Object[]{user.getId(), user.getUsername(), user.getEmail()}
);
}
@Test
public void testSelectById_1 () {
Mockito.when(userService.selectById(Mockito.nullable(Long.class))).thenReturn(new User(0L, "mock-username", "mock-email"));
User user = userController.userInfo(null);
Assert.assertArrayEquals(
new Object[]{0L, "mock-username", "mock-email"},
new Object[]{user.getId(), user.getUsername(), user.getEmail()}
);
}
}
上面的第一个单元测试在mockito 1.x运行良好,但在2.x中会出错,一般来说都是报空指针或者进了错误的待测代码分支。
集合匹配
和简单的基本类型匹配器一样,如果要匹配任意集合,可以使用anyList(); anyMap(); anySet(); anyCollection()
等方法匹配,但如果需要匹配有几个元素等等,可能必须使用自定义的参数匹配器:
@RunWith(MockitoJUnitRunner.class)
public class CollectionArgumentMatcher {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void testAnyList () {
Mockito.when(userDao.batchInsert(Mockito.anyList())).thenThrow(new MockitoException("test anyList()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("test anyList()");
userService.batchInsert(Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com")));
}
@Test
public void testListSize () {
Mockito.when(userDao.batchInsert(Mockito.argThat(users -> users.size() == 2))).thenThrow(new MockitoException("test match list size"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("test match list size");
userService.batchInsert(Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com")));
}
@Test
public void testListType () {
Mockito.when(userDao.batchInsert(Mockito.argThat(users -> users instanceof LinkedList))).thenThrow(new MockitoException("test match list type"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("test match list type");
userService.batchInsert(new LinkedList<>(Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com"))));
}
}
这里使用了argThat()
方法,传入一个匿名的ArgumentMatcher
,当然可以在把ArgumentMatcher
定义在外部,方便复用。
有了argThat()
方法后,感觉一切参数都可以被匹配到了,但很多时候还有其他更简单的方法。
对象匹配
如果仅仅使用前面出现过的方法,对象匹配有三种情况和相应的解决方案:
- 任意对象:使用
any()
或any(Claas<T>)
方法即可 - 部分字段或全部字段匹配:使用
argThat()
方法自定义参数匹配器 equals()
方法匹配:使用eq()
方法即可
第一种和第三种方法确实没错,但第二种可以有其他方法:
@RunWith(MockitoJUnitRunner.class)
public class ObjectArgumentMatcher {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void testAny () {
Mockito.when(userDao.insert(Mockito.any())).thenThrow(new MockitoException("test any()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test any()"));
userService.insert(new User( "test", "test@qq.com"));
}
@Test
public void testAnyClass () {
Mockito.when(userDao.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test any(Class<T>)"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test any(Class<T>)"));
userService.insert(new User( "test", "test@qq.com"));
}
@Test
public void testArgThat () {
Mockito.when(userDao.insert(Mockito.argThat(new UsernameAndEmailMatcher()))).thenThrow(new MockitoException("test argThat()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test argThat()"));
userService.insert(new User( "test11", "test@qq.com"));
}
@Test
public void testRefEq () {
User matchUser = new User(0L,"test", "test@qq.com");
Mockito.when(userDao.insert(Mockito.refEq(matchUser, "id"))).thenThrow(new MockitoException("test refEq()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test refEq()"));
userService.insert(new User( "test", "test@qq.com"));
}
@Test
public void testEq () {
User matchUser = new User("test", "test@qq.com");
Mockito.when(userDao.insert(Mockito.eq(matchUser))).thenThrow(new MockitoException("test eq()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test eq()"));
userService.insert(new User( "test", "test@qq.com"));
}
private static class UsernameAndEmailMatcher implements ArgumentMatcher<User> {
@Override
public boolean matches(User user) {
return user.getUsername().contains("test") && user.getEmail().equals("test@qq.com");
}
}
}
其中refEq()
方法反射比较,先创建一个待比较的对象,并且可以传入变长参数:忽略的字段列表,所有需要忽略的字段不参与反射取值和比较。
argThat()
的自由度最高,每个字段可以自由使用任何其他匹配方式,比如上面的例子匹配username
的时候使用的contains("test")
而不是equals()
,但其他方式的对相匹配只能是equals()
空对象匹配
可以使用nullable(Class<T>)
匹配包含空对象的任意对象,如果要匹配null可以使用isNull()
:
@RunWith(MockitoJUnitRunner.class)
public class NullArgumentMatcher {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void testNullable () {
Mockito.when(userDao.insert(Mockito.nullable(User.class))).thenThrow(new MockitoException("test nullable()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test nullable()"));
userService.insert(new User( "test", "test@qq.com"));
}
@Test
public void testNull () {
Mockito.when(userDao.delete(Mockito.isNull())).thenThrow(new MockitoException("test isNull()"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test isNull()"));
userService.delete(null);
}
}
返回值为空
当一个方法的返回值为空的时候如何打桩Mock呢,一般情况下,如果对象被@Mock
注解,它的所有public
方法将被全部mock,有返回值的返回null,没有返回值的啥事儿也不做,所以如果没有返回值的情况下,如果mock该方法不做任何操作,可以仅仅使用@Mock
注解即可,如果需要做其它事情,比如抛出异常、捕获参数并断言等,可以用下面的方法mock:
@RunWith(MockitoJUnitRunner.class)
public class ReturnVoidMock {
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void testVoidMock () {
Mockito.doNothing().when(userService).delete(Mockito.anyLong());
userController.close(new User(){{setId(1L);}});
}
@Test
public void testVoidMock_1 () {
Mockito.doThrow(new MockitoException("test void throw")).when(userService).delete(Mockito.nullable(Long.class));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test void throw"));
userController.close(new User());
}
}
根据不同参数返回不同值
不可以在同一个测试方法中对同一个对象的同一对象mock两次,在mockito 2.x中会报空指针异常,并且提示第一次mock无效;那如果待测方法调了另一个对象的方法两次,并且两次的参数不一样,返回值也必须不一样,可以这样mock:
@RunWith(MockitoJUnitRunner.class)
public class ConditionMock {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
/**
* java.lang.NullPointerException
[MockitoHint] ConditionMock.testConditionReturn_error (see javadoc for MockitoHint):
[MockitoHint] 1. Unused... -> at github.clyoudu.match.ConditionMock.testConditionReturn_error(ConditionMock.java:52)
[MockitoHint] ...args ok? -> at github.clyoudu.match.ConditionMock.testConditionReturn_error(ConditionMock.java:53)
*/
@Test
@Ignore
public void testConditionReturn_error() {
Mockito.when(userDao.insert(Mockito.argThat(user -> user.getUsername().equals("Bob")))).thenReturn(new User());
Mockito.when(userDao.insert(Mockito.argThat(user -> user.getUsername().equals("David")))).thenThrow(new MockitoException("Username David not allowed to be registered"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("Username David not allowed to be registered");
List<User> users = Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com"));
userService.batchInsert2(users);
}
@Test
public void testConditionReturn() {
Mockito.when(userDao.insert(Mockito.any())).thenAnswer(invocationOnMock -> {
User user = invocationOnMock.getArgument(0);
switch (user.getUsername()) {
case "Bob":
user.setId(999L);
return user;
case "David":
throw new MockitoException("Username David not allowed to be registered");
default:
return null;
}
});
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("Username David not allowed to be registered");
List<User> users = Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com"));
userService.batchInsert2(users);
Assert.assertEquals(new Long(999), users.get(0).getId());
}
@Test
public void testConditionReturn_1() {
Mockito.when(userDao.insert(Mockito.any())).thenReturn(
new User(999L, "Bob", "bob@aa.com"),
new User(1000L, "David", "david@bb.com")
);
List<User> users = Arrays.asList(new User("Bob", "bob@aa.com"), new User("David", "david@bb.com"));
Assert.assertEquals(2, userService.batchInsert2(users));
}
}
- 使用
thenAnswer()
,反射得到参数,根据不同参数返回不同结果即可。 - 使用
thenReturn()
直接返回多个结果,如果thenReturn()
返回多个结果,表示那么该方法被调用几次就返回第几个结果,如果调用次数超过返回结果数,则默认返回null
其中thenAnswer()
更加灵活,因为可以对任意条件做任何事,包括修改参数、抛出异常、返回结果等,而thenReturn()
只能返回结果。
Mock&Spy
正如上面提到过的,Mock
注解会默认mock类的所有public方法,使没有返回值的方法什么事儿也不做、有返回值的方法返回null,但有时候我们并不想mock一些方法,比如这些方法没有依赖任何其他对象,默认mock显得多此一举,而@Spy
注解刚好可以解决这个问题:
@RunWith(MockitoJUnitRunner.class)
public class SpyTest {
@InjectMocks
private UserController userController;
@Spy
private UserServiceImpl userService;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
/**
* NPE when use
* @Mock
* private UserServiceImpl userService;
* because getUsername() returns null
*/
@Test
public void testSpy () {
Mockito.doThrow(new MockitoException("test @Spy")).when(userService).insert(Mockito.any(User.class));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("test @Spy");
User user = userController.register("aa@b.com");
Assert.assertEquals("aa", user.getUsername());
}
}
可以看到getUsername()
调用了真实的方法,而insert(User)调用了mock方法,但注意使用@Spy
时,声明mock的时候不再和@Mock
相同,必须使用Mockito.doXXX().when(someObj).someMethod(someArgumentMather)
,原因是:
- 使用
@Mock
注解的类,所有方法都不是真实的方法,而且返回值都是NULL。 - 使用
@Spy
注解的类,所有方法都是真实方法,返回值都是和真实方法一样的。 - 所以,你用when去设置模拟返回值时,它里面的方法(userService.insert(User))会先执行一次。
- 使用doReturn或doThrow去设置的话,就不会产生上面的问题,因为有when来进行控制要模拟的方法,所以不会执行原来的方法。
Mock static/final/private/protected方法
mockito
的原理是继承或实现原本的类,覆盖所有public
方法默认全部返回null或空方法体,如果有设置某一个方法的期望,就按照期望来重写。显而易见,static
方法无法被重写,那么mockito
是不可能mock静态方法的,同理final
private
方法也不能被重写,因此这两种方法也不能被被mockito
mock;而protected
方法不能被对象实例直接调用,所以protected
方法也不能直接使用mockito
显示调用protected
方法来mock,这几类方法只能使用powemock来mock,因为powermock的原理是修改类的class文件,比如去除final标识等,然后替换方法为虚拟实现,再把这个class用org.powermock.core.classloader.MockClassLoader
加载,调用的时候使用MockClassLoader
加载的类,而不是系统加载的类:
@RunWith(MockitoJUnitRunner.class)
public class MockitoFinalStaticTest {
@Mock
private IdGenerator idGenerator;
/**
* Also, this error might show up because you use argument matchers with methods that cannot be mocked.
* Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
* Mocking methods declared on non-public parent classes is not supported.
*/
@Test
public void testFinal () {
Mockito.when(idGenerator.generated(Mockito.anyLong())).thenReturn(false);
Assert.assertTrue(!idGenerator.generated(100L));
}
/**
* Also, this error might show up because:
* 1. you stub either of: final/private/equals()/hashCode() methods.
* Those methods *cannot* be stubbed/verified.
* Mocking methods declared on non-public parent classes is not supported.
* 2. inside when() you don't call method on mock but on some other object.
*/
@Test
public void testStatic () {
Mockito.when(IdGenerator.getId()).thenReturn(100L);
Assert.assertEquals(new Long(100L), IdGenerator.getId());
}
}
用mockito
mockstatic
和final
方法的时候会报错。
@RunWith(PowerMockRunner.class)
@PrepareForTest(IdGenerator.class)
public class PowerMockitoTest {
@Mock
private IdGenerator idGenerator;
@Spy
private IdGenerator idGeneratorPower = new IdGenerator();
@Before
public void before() {
PowerMockito.mockStatic(IdGenerator.class);
}
@Test
public void testFinal () {
PowerMockito.when(idGenerator.generated(Mockito.anyLong())).thenReturn(false);
Assert.assertTrue(!idGenerator.generated(1L));
PowerMockito.when(idGenerator.generated(Mockito.anyLong())).thenReturn(true);
Assert.assertTrue(idGenerator.generated(1L));
}
@Test
public void testStatic () {
PowerMockito.when(IdGenerator.getId()).thenReturn(100L);
Long id = IdGenerator.getId();
PowerMockito.verifyStatic(IdGenerator.class, Mockito.times(1));
IdGenerator.getId();
Assert.assertEquals(new Long(100L), id);
}
@Test
public void testPrivate () throws Exception {
PowerMockito.doReturn(100L).when(idGeneratorPower, "getCurrentId1");
Assert.assertEquals(new Long(100L), idGeneratorPower.getCurrentIdViaPrivate());
}
@Test
public void testProtected () throws Exception {
PowerMockito.doReturn(100L).when(idGeneratorPower, "getCurrentId2");
Assert.assertEquals(new Long(100L), idGeneratorPower.getCurrentIdViaProtected());
}
}
和mockito
的区别如下
- 使用
@RunWith(PowerMockRunner.class)
; - 使用
@PrepareForTest(IdGenerator.class)
; @Before
方法里使用PowerMockito.mockStatic(IdGenerator.class);
正常情况下这些方法都不应该被mock,static
final
private
protected
方法都应算作内部契约,只有在这些方法很难被构造或者依赖于环境、无法生成固定值的时候才可能可以被mock。
断言(验证)
简单断言
形如
@RunWith(PowerMockRunner.class)
@PrepareForTest(IdGenerator.class)
public class SimpleAssert {
@Before
public void before() {
PowerMockito.mockStatic(IdGenerator.class);
}
@Test
public void testStatic () {
PowerMockito.when(IdGenerator.getId()).thenReturn(100L);
Assert.assertEquals(new Long(100L), IdGenerator.getId());
}
}
这种断言就是简单断言,能用这种断言的方法有如下特点:
- 有返回值;
- 返回值为简单类型;
- 只需要对返回值某一属性或特点断言
正常情况下,大部分断言都是类似的简单断言。
复杂对象断言
如果方法的返回值是一个对象,对象有很多字段,某个字段还是一个集合甚至是另外一个对象,这时候有很多种断言方法:
@RunWith(MockitoJUnitRunner.class)
public class ObjectAssert {
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Test
public void testSelectById_1 () {
Mockito.when(userService.selectById(Mockito.nullable(Long.class))).thenReturn(new User(0L, "mock-username", "mock-email"));
User user = userController.userInfo(null);
Assert.assertEquals(new Long(0), user.getId());
Assert.assertEquals("mock-username", user.getUsername());
Assert.assertEquals("mock-email", user.getEmail());
}
@Test
public void testSelectById_2 () {
Mockito.when(userService.selectById(Mockito.nullable(Long.class))).thenReturn(new User(0L, "mock-username", "mock-email"));
User user = userController.userInfo(null);
Assert.assertArrayEquals(
new Object[]{0L, "mock-username", "mock-email"},
new Object[]{user.getId(), user.getUsername(), user.getEmail()}
);
}
@Test
public void testSelectById_3 () {
Mockito.when(userService.selectById(Mockito.nullable(Long.class))).thenReturn(new User(0L, "mock-username", "mock-email"));
User user = userController.userInfo(null);
Assert.assertThat(user, new UserMatcher(new User(0L, "mock-username", "mock-email")));
}
private class UserMatcher extends BaseMatcher<User> {
private User expectedUser;
public UserMatcher(User expectedUser) {
this.expectedUser = expectedUser;
}
@Override
public boolean matches(Object o) {
User actualUser = (User) o;
return expectedUser.getId().equals(actualUser.getId()) &&
expectedUser.getUsername().equals(actualUser.getUsername()) &&
expectedUser.getEmail().equals(actualUser.getEmail());
}
@Override
public void describeTo(Description description) {
description.appendValue("Actual User is not match expected User");
}
}
}
断言异常
有时候我们需要捕获方法抛出的异常,并对异常断言以确保抛出了正确的异常,junit有很多种断言异常的方法:
@RunWith(MockitoJUnitRunner.class)
public class ExceptionAsserts {
@InjectMocks
private UserController userController;
@Mock
private UserService userService;
@Rule
public ExpectedException expectedEx = ExpectedException.none();
@Test
public void testException () {
try{
Mockito.when(userService.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test exception"));
userController.register(new User());
} catch (Exception e) {
Assert.assertTrue(e instanceof MockitoException);
Assert.assertEquals("test exception", e.getMessage());
}
}
@Test
public void testException_1 () {
try{
Mockito.when(userService.insert(Mockito.any(User.class))).thenReturn(new User());
userController.register(new User());
} catch (Exception e) {
Assert.assertTrue(e instanceof MockitoException);
Assert.assertEquals("test exception", e.getMessage());
}
}
@Test
public void testException1 () {
try{
Mockito.when(userService.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test exception"));
userController.register(new User());
Assert.fail();
} catch (Exception e) {
Assert.assertTrue(e instanceof MockitoException);
Assert.assertEquals("test exception", e.getMessage());
}
}
@Test(expected = MockitoException.class)
public void testException2 () {
Mockito.when(userService.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test exception"));
userController.register(new User());
}
@Test
public void testException3 () {
Mockito.when(userService.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test exception"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage("test exception");
userController.register(new User());
}
/**
* Error
*/
@Test
public void testException4 () {
Mockito.when(userService.insert(Mockito.any(User.class))).thenThrow(new MockitoException("test exception extra"));
expectedEx.expect(MockitoException.class);
expectedEx.expectMessage(Is.is("test exception"));
userController.register(new User());
}
}
- 对于
testException_1
和testException_1
,会有一个漏洞:当try调用方法没有抛出异常时,单元测试依然能通过,并没有进入catch块; testException1
可以弥补第一种方法的不足:在方法调用后使用Assert.fail()
可以保证当测试没有进入catch块时会报断言错误,测试不通过;- 一般来说,单元测试不应该使用条件分支和流程控制,治理如果直接使用try catch不是很优雅的测试方法,可以使用
testException2
在@Test
中指定expected
异常类型,但只能断言异常类型,并不能对message断言; testException3
完美解决了上面两个问题,是比较推荐的异常断言方式,但注意在断言message时,只要message包含声明的字符串都会返回成功;testException4
演示了如何断言meesage与声明的字符串完全匹配,当然还有更多的匹配方式,也可以自定义匹配方式。
断言参数
有时候一个方法没有返回值,或者我们对返回值并不感兴趣,并且这个方法的参数是调用其他方法生成的,如何断言这个方法的参数是否正确呢?可能有点绕,直接看测试:
@RunWith(MockitoJUnitRunner.class)
public class ParameterAsserts {
@InjectMocks
private UserController userController;
@Spy
private UserServiceImpl userService;
@Captor
private ArgumentCaptor<User> userArgumentCaptor;
@Test
public void testParameter () {
Mockito.doReturn(new User()).when(userService).insert(Mockito.any(User.class));
userController.register("aaa@bb.com");
Mockito.verify(userService).insert(userArgumentCaptor.capture());
Assert.assertEquals("aaa", userArgumentCaptor.getValue().getUsername());
}
}
这里需要断言调用insert(String email)
方法有没有正确获取用户名,而有必须mockinsert(User)
方法,导致insert(User)
方法的返回值不能作为断言的依据,只能去要捕获insert(User)
参数,对该参数断言,判断获取的用户名是否正确。
断言方法调用次数
有时候只需要断言方法是否被调用或者只关心方法是否被调用时,可以这样断言:
@RunWith(MockitoJUnitRunner.class)
public class VerifyAsserts {
@InjectMocks
private UserServiceImpl userService;
@Mock
private UserDao userDao;
@Test
public void testVerifyTimes () {
userService.batchInsert2(new ArrayList<>());
Mockito.verify(userDao, Mockito.never()).insert(Mockito.any(User.class));
}
@Test
public void testVerifyTimes1 () {
Mockito.when(userDao.insert(Mockito.any(User.class))).thenReturn(new User());
userService.batchInsert2(Collections.singletonList(new User()));
Mockito.verify(userDao, Mockito.times(1)).insert(Mockito.any(User.class));
Mockito.verify(userDao, Mockito.atMost(1)).insert(Mockito.any(User.class));
Mockito.verify(userDao, Mockito.atLeast(1)).insert(Mockito.any(User.class));
}
}
扩展
其实PowerMockito
是Mockito
的扩展,两者又可以非常放便地和JUnit结合使用,这得益于JUnit的Runner
模式:使用@RunWith()
注解指定特定的Runner,就可以按照Runner
中设定的方式加载、初始化、运行单元测试。
那么基于这个Runner
模式我们能不能自定义一个Runner
来解决目前JUnit
、PowerMockito
和Mockito
都无法解决的问题呢,我觉得是可以的:
- 断言异常非常不方便:继承JUnit默认的Runner,增强一下对
@Test
注解的反射支持,比如重新定义一个@ExceptionTest
注解,可以在注解中定义异常类型和message; - 参数外置话做的不是很好:很多时候我们会对同一个代码分支用大量的测试用例去验证,但遵循单元测试中不应该有流程控制和循环,并且一个单元测试最好只有一个断言的原则,多少个用例就意味着多少个测试方法,但这些方法几乎是一样的方法体。即使不遵循这样的原则,多个断言也让单元测试难以维护,那么可以扩展一个
Runner
,让其支持参数外置化,这样的Runner其实已经有实现,具体参考:【单元测试】JUnit参数化测试的讨论
一般来说,任何JUnit单元测试不支持的情况,或者使用JUnit难以遵循单元测试原则的,都可以尝试使用自定义Runner解决这个问题。