模拟框架Mockito
主体不算复杂,各种根据业务的组合
日常做单元测试,当遇到依赖当第三方接口尚无法对接、或者依赖数据库、缓存等服务不易测试时,可以使用Mock。常用的框架是Mockito,一方面使用Mockito可以屏蔽依赖接口并返回Mock数据,使得双方的开发得以同步进行(确定接口的协议)编码,另一方面使用Mockito验证业务逻辑,当日后更改到某处代码即可回归测试用例看改动是否覆盖到所有的测试点,因此使用Mockito不单单能保证代码的质量,更能提高代码维护性、提前发现代码的bug。
Mock四要素
- 什么是Mock
- 为什么需要Mock:Mock是为了解决units、代码分层开发之间由于耦合而难于被测试的问题,所以mock object是单元测试的一部分
- Mock的好处是什么:提前创建测试,提高代码质量、TDD(测试驱动开发)
- 并行工作:创建一个验证或者演示程序,为无法访问的资源编写测试
什么是Mockito
Mockito是一个非常优秀的模拟框架,可以使用它简洁的API来编写漂亮的测试代码,它的测试代码可读性高同时会产生清晰的错误日志。
引入依赖:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.5.1</version>
<scope>test</scope>
</dependency>
一套组合拳
- @Runwith(MockitoJunitRunner.class) 初始化 被下面注解修饰的bean .
- @Mock 模仿一个bean
- @Spy 同等的bean
- @InjectMocks 依赖的对象会一次注入
- @Captor 获取调用的方法的参数
通常有两种方式引入mockito来进行mock:
- 注解
@RunWith(MockitoJUnitRunner.class)
public class TestMockito {
@Mock
private AbstractConsumer abstractConsumer;
@Test
public void testMock() {
abstractConsumer.execute("");
}
}
- 代码方式
public class TestMockito2 {
@Mock
private AbstractConsumer abstractConsumer;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void test() {
abstractConsumer.execute("");
}
}
- 当Mock对象调用方法返回对象也需要Mock时,可以使用@Mock注解中answer变量(待验证)
public class DeepMockTest {
//通过answer方式可以完成深度mock
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
public IndexController indexController;
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void deepTest() {
indexController.helloStudent("").toString();
}
}
一个示例
数据库服务
@Repository
public class DataService {
public List<String> getAll() {
System.out.println("get All from database ");
return Collections.emptyList();
}
}
业务逻辑服务
@Service
public class SomeBusiness {
@Autowired
DataService dataService;
public List<String> fetchAllData() {
return dataService.getAll();
}
}
mock 依赖数据库返回的数据(业务逻辑服务依赖 数据库的服务)
@RunWith(MockitoJUnitRunner.class)
public class TestTutorialApplicationTests {
@Mock
DataService dataService;
@InjectMocks
SomeBusiness someBusiness;
@Test
public void testFetchAll() {
// 定义当调用指定方法时的,Mock值 stub
when(dataService.getAll()).thenReturn(Arrays.asList("1", "2", "3"));
Assert.assertEquals(someBusiness.fetchAllData(),
Arrays.asList("1", "2", "3"));
}
}
@Mock 返回一个模仿的对象
@InjectMocks 将@Mock修饰的对应对象注入 即是给 someBusiness 注入对象 dataService
.@InjectMocks 不能修饰接口, 只能是 类
mockito.when (…).thenReturn(…) 当调用某个方法时,返回固定的数(指定调用与Mock值)既是返回数据库的服务的数据
@RunWith(MockitoJunitRunner) 初始化 被@Mock ,@Spy @InjectMocks 修饰的对象
Mockito.verify(someclass).someMethod 验证某个方法的调用次数 默认是1
verify(MockObject,time(1)).someMethod();
可以用来验证void 方法, 就是验证 该方法调用了几次。
void 方法做单元 测试的时候, 可以用作被调用多verify() 验证是否被调用的次数
@RunWith(MockitoJUnitRunner.class)
public class TestTutorialApplicationTests {
@Mock
List<String> mockList = new ArrayList<>();
@Captor
ArgumentCaptor<String> argumentCaptor;
@Test
public void whenUseCaptorAnnotation() {
mockList.add("one");
Mockito.verify(mockList).add(argumentCaptor.capture());
Assert.assertEquals("one", argumentCaptor.getValue());
}
ArgumentCaptor 捕捉调用的参数
验证对象的行为Verify
可以验证次数、参数等,不要被上一段搞混了
@Test
public void testVerify(){
//创建mock
List mockedList = mock(List.class);
mockedList.add("1");
mockedList.clear();
//验证list调用过add的操作行为
verify(mockedList).add("1");
//验证list调用过clear的操作行为
verify(mockedList).clear();
//使用内建anyInt()参数匹配器,并存根,get方法传入任何int时都返回element
when(mockedList.get(anyInt())).thenReturn("element");
System.out.println(mockedList.get(2)); //此处输出为element
verify(mockedList).get(anyInt());}
存根—stub
stubbing完全是模拟一个外部依赖,用来提供测试时所需要的测试数据。调用某些方法时,想预先设定返回结果,可以通过stub来实现。
- 调用mock对象中有返回值时,录制预期结果、调用、验证调用
@RunWith(MockitoJUnitRunner.class)
public class StubbingTest {
public List list;
@Before
public void init() {
list = mock(List.class);
}
/**
* 对调用有返回值的函数调用stub预期结果
*/
@Test
public void test() {
//stub
when(list.get(0)).thenReturn("first");
//验证
assertThat(list.get(0),equalTo("first"));
//stub anyInt , then throw exception
when(list.get(anyInt())).thenThrow(new RuntimeException());
try{
list.get(0); //会抛出异常
} catch (Exception e) {
assertThat(e, instanceOf(RuntimeException.class));
}
}
@After
public void destory() {
reset(list);
}
}
- 调用mock对象中无返回值的方法时,如何验证是否调用、验证抛出异常
@Test
public void noReturnStubTest() {
//什么都不做
doNothing().when(list).clear();
//调用
list.clear();
//验证是否执行过一次
verify(list,times(1)).clear();
//调用无返回值clear时抛出异常
doThrow(RuntimeException.class).when(list).clear();
try {
list.clear();
} catch (Exception e) {
assertThat(e, instanceOf(RuntimeException.class));
}
//验证是否调用过
verify(list,times(2)).clear();
}
- 调用mock对象有返回值的函数,调用多次时,可以预设置每一次返回的结果
/**
* stub相同方法的不同调用次数时的返回值
*/
@Test
public void MultiCallReturnMethodStubTest() {
when(list.size()).thenReturn(1,2,3,4);
assertThat(list.size(),equalTo(1));
assertThat(list.size(),equalTo(2));
assertThat(list.size(),equalTo(3));
assertThat(list.size(),equalTo(4));
}
- 调用mock对象由参数的方法时,需要根据参数来返回不同的返回值,需要thenanswer来完成
@Test
public void MultiCallArgMethodReturnStubTest() {
when(list.get(anyInt())).thenAnswer(invocation -> {
Integer argument = invocation.getArgument(0);
return String.valueOf(argument * 10);
});
assertThat(list.get(1),equalTo("10"));
assertThat(list.get(99),equalTo("990"));
}
@Test
public void testStub() {
when(mock.someMethod("some arg"))
.thenThrow(new RuntimeException())
.thenReturn("foo");
mock.someMethod("some arg");
//第一次调用:抛出运行时异常
//第二次调用: 打印 "foo"
System.out.println(mock.someMethod("some arg"));
//任何连续调用: 还是打印 "foo" (最后的存根生效).
System.out.println(mock.someMethod("some arg"));
//可供选择的连续存根的更短版本:
when(mock.someMethod("some arg")).thenReturn("one", "two", "three");
when(mock.someMethod(anyString())).thenAnswer(new Answer() {
Object answer(InvocationOnMock invocation) {
Object[] args = invocation.getArguments();
Object mock = invocation.getMock();
return "called with arguments: " + args;
}
});
// "called with arguments: foo
System.out.println(mock.someMethod("foo"));
}
- 调用mock对象时可以stub预期结果,可以调用到mock的源对象本身方法,通过thenCallRealMethod
@Test
public void callRealMethodTest() {
//mock是通过cglib生成代理对象
Service service = mock(Service.class);
//调用mock方法时stub返回值
when(service.callMock()).thenReturn("mock123");
assertThat(service.callMock(),equalTo("mock123"));
//调用mock对象本身的方法。真实调用了
when(service.callReal()).thenCallRealMethod();
assertThat(service.callReal(),equalTo(10));
}
- 存根(stub)可以覆盖:例如测试方法可以覆盖通用存
- 一旦做了存根方法将总是返回存根的值,无论这个方法被调用多少次
参数匹配器
@Test
public void testArugument{
//使用内建anyInt()参数匹配器
when(mockedList.get(anyInt())).thenReturn("element");
System.out.println(mockedList.get(999)); //打印 "element"
//同样可以用参数匹配器做验证
verify(mockedList).get(anyInt());
//注意:如果使用参数匹配器,所有的参数都必须通过匹配器提供。
verify(mock)
.someMethod(anyInt(), anyString(), eq("third argument"));
//上面是正确的 - eq(0也是参数匹配器),而下面的是错误的,第三个参数未由匹配器提供
verify(mock)
.someMethod(anyInt(), anyString(), "third argument");
}
验证精确调用次数/至少X次/从不
@Test
public void testVerify{
List<String> mockedList = new ArrayList();
mockedList.add("once");
mockedList.add("twice");
mockedList.add("twice");
mockedList.add("three times");
mockedList.add("three times");
mockedList.add("three times");
//下面两个验证是等同的 - 默认使用times(1)
verify(mockedList).add("once");
verify(mockedList, times(1)).add("once");
verify(mockedList, times(2)).add("twice");
verify(mockedList, times(3)).add("three times");
//使用using never()来验证. never()相当于 times(0)
verify(mockedList, never()).add("never happened");
//使用 atLeast()/atMost()来验证
verify(mockedList, atLeastOnce()).add("three times");
verify(mockedList, atLeast(2)).add("five times");
verify(mockedList, atMost(5)).add("three times");
}
验证调用顺序
@Test
public void testOrder(){
// A. 单个Mock,方法必须以特定顺序调用
List singleMock = mock(List.class);
//使用单个Mock
singleMock.add("was added first");
singleMock.add("was added second");
//为singleMock创建 inOrder 检验器
InOrder inOrder = inOrder(singleMock);
//确保add方法第一次调用是用"was added first",然后是用"was added second"
inOrder.verify(singleMock).add("was added first");
inOrder.verify(singleMock).add("was added second");
}
spy
spy也是对目标对象进行mock,但是只有设置了stub的方法才会mock,其他方法直接调用目标对象本身的方法,有两种方式代码方式和annotation方式。(mock方法和spy方法都可以对对象进行mock。但是前者是接管了对象的全部方法,而后者只是将有桩实现(stubbing)的调用进行mock,其余方法仍然是实际调用。)
- 代码方式
@RunWith(MockitoJUnitRunner.class)
public class SpyTest {
@Test
public void spyTest() {
//创建实例对象
List<Integer> realList = new ArrayList<>();
//spy包装实例对象
List<Integer> list = spy(realList);
//对spy对象设置stub
when(list.isEmpty()).thenReturn(true);
when(list.size()).thenReturn(10);
//调用realList中的真实方法
list.add(1);
list.add(2);
assertThat(list.get(0),equalTo(1));
assertThat(list.get(1),equalTo(2));
assertThat(list.size(),equalTo(10));
assertThat(list.isEmpty(),equalTo(true));
}
}
- 注解方式
public class SpyAnnotationTest { @Spy public List<Integer> list = new ArrayList<>(); @Before public void init() { MockitoAnnotations.initMocks(this); } @Test public void test() { when(list.size()).thenReturn(1); when(list.isEmpty()).thenReturn(true); list.add(1); list.add(2); assertThat(list.get(0),equalTo(1)); assertThat(list.get(1),equalTo(2)); assertThat(list.isEmpty(),equalTo(true)); assertThat(list.size(),equalTo(1)); } }
spring context 的单元测试
一套组合拳
- @SpringBootTest 当需要start一个SpringBoot Container时,会把这个与@Autowired一起使用
- @MockBean 它会mock 一个对象,排除 spring context 中的其它实现类
- 总结就是 @SpringBootTest + @MockBean + @Autowired
// @RunWith(SpringRunner.class) // @SpringBootTest @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) // 这里默认是WebEnvironment.MOCK public class SpringBootBusinessTest { @MockBean DataService dataService; @Autowired SomeBusiness business; @Test public void testSpringMock() { when(dataService.getAll()).thenReturn(Arrays.asList("1", "2", "3")); Assert.assertEquals(business.fetchAllData(), Arrays.asList("1", "2", "3")); } }
@RunWith(StringRunner.class) @SpringBootTest 在单元测试中启动Spring Context。在JUnit5有变更。
当不需要SpringBoot Container时,使用下面这种方式
- @ExtendWith(MockitoExtension.class)
- @Mock来mock一个bean及stub
- @InjectMocks来Inject一个bean并用上面的mock的bean来define其依赖(可以看下面的说明)
- @ExtendWith + @Mock + @InjectMocks
Mockito 2.x + JUnit 5要这样配置。JUnit 5与之前相比变更很多,有空梳理一下。主体Bean使用@InjectMocks标注,该Bean依赖的其他Bean可通过@Mock标注,否则这些的值会是null,并且可以对这些bean的调用mock stub
@ExtendWith(MockitoExtension.class)
public class TestTutorialApplicationTests {
@Mock
DataService dataService;
@InjectMocks
SomeBusiness someBusiness;
// 原来的@Before、@After更细致一些。
// @BeforeAll
// @BeforeEach
// @AfterAll
// @AfterEach
@Test
public void testFetchAll() {
// 定义当调用指定方法时的,Mock值 stub
when(dataService.getAll()).thenReturn(Arrays.asList("1", "2", "3"));
Assertions.assertEquals(Arrays.asList("1", "2", "3"), someBusiness.fetchAllData());
}
}
静态方法
新加依赖
<!--Mockito's inline mock maker supports static mocks based on the Instrumentation API.
You can simply enable this mock mode, by placing the 'mockito-inline' artifact where you are currently using 'mockito-core'.
Note that Mockito's inline mock maker is not supported on Android.-->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
public class StaticBS {
public static String pr() {
return "Local";
}
}
@Test
public void testStatic() {
System.out.println(StaticBS.pr()); // 原值Local
try (var bsMockedStatic = Mockito.mockStatic(StaticBS.class)) { // 这个得关闭
// when(StaticBS::pr) 和 when(() -> StaticBS.pr()) 一样
bsMockedStatic.when(StaticBS::pr).thenReturn("First Try").thenReturn("Last Try").thenCallRealMethod().thenThrow(new RuntimeException("别调了"));
System.out.println(StaticBS.pr()); // First Try
System.out.println(StaticBS.pr()); // Last Try
System.out.println(StaticBS.pr()); // 原值Local
System.out.println(StaticBS.pr()); // 抛出异常
}
}
引用: