为什么要用单元测试
假如有一个非常大的项目,这个项目代码逻辑很复杂的情况,如何让它在特定的条件和入参执行到一块具体的代码逻辑,直接运行项目进行测试的代价是很高的,单元测试就可以快速的定位到具体逻辑去测试这块逻辑是否有问题。
什么是单元测试
单元测试就是对一个类的行为进行测试,通俗来说,每个方法都有入参和返回值,输入不同的参数会得到不同的返回值,或者异常;那么单元测试就是根据不同的use case来模拟不同场景执行,验证我们的代码在不同的执行路径下,是否会返回预期的结果。
单元测试是相互独立的,不应该依赖于外部资源(DB、第三方接口、网络链接),在任何时间任何环境下执行结果都是相同的。
Mockito
在日常工作中,我们基本都会基于spring这种容器框架来做开发,并且大部分情况下,项目都会依赖外部系统,比如DB、分布式缓存、第三方接口等。那么我们在针对某个类进行单元测试的时候,就会遇到各种困难:操作DB造成脏数据、第三方接口测试环境不可达等等。这时候,我们就需要一种方式屏蔽这些依赖组件对测试的影响,mockito就是这样一种mock工具,帮我们解决上述这些问题,当然类似的工具还有easymock、powermock、基于groovy的Spock等。
pom依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
案例:模拟登录
// An highlighted block
public class Account {
}
public class AccountDao {
public Account findAccount(String username,String password){
//假设DB不可用
throw new UnsupportedOperationException();
}
}
public class AccountLoginController {
private AccountDao accountDao;
public AccountLoginController(AccountDao accountDao) {
this.accountDao = accountDao;
}
public String login(HttpServletRequest request){
final String username = request.getParameter("username");
final String password = request.getParameter("password");
try {
Account account = accountDao.findAccount(username, password);
if(account == null){
return "/login";
}else {
return "/index";
}
}catch (Exception e){
return "505";
}
}
}
@RunWith(MockitoJUnitRunner.class)
public class AccountLoginControllerTest {
private AccountDao accountDao;
private HttpServletRequest request;
private AccountLoginController controller;
@Before
public void setUP(){
this.accountDao = Mockito.mock(AccountDao.class);
this.request = Mockito.mock(HttpServletRequest.class);
this.controller = new AccountLoginController(accountDao);
}
@Test
public void testLoginSuccess(){
Account account = new Account();
when(request.getParameter("username")).thenReturn("Allen");
when(request.getParameter("password")).thenReturn("123456");
when(accountDao.findAccount(anyString(),anyString())).thenReturn(account);
assertThat(controller.login(request),equalTo("/index"));
}
@Test
public void testLoginFailuer(){
when(request.getParameter("username")).thenReturn("Allen");
when(request.getParameter("password")).thenReturn("123456");
when(accountDao.findAccount(anyString(),anyString())).thenReturn(null);
assertThat(controller.login(request),equalTo("/login"));
}
@Test
public void testLogin505(){
when(request.getParameter("username")).thenReturn("Allen");
when(request.getParameter("password")).thenReturn("123456");
when(accountDao.findAccount(anyString(),anyString())).thenThrow(UnsupportedOperationException.class);
assertThat(controller.login(request),equalTo("505"));
}
}
Mock的三种方式
1. @RunWith(MockitoJUnitRunner.class)
// An highlighted block
@RunWith(MockitoJUnitRunner.class)
public class MockByRunner {
@Test
public void testMock(){
AccountDao accountDao = mock(AccountDao.class);
Account account = accountDao.findAccount("x", "x");
System.out.println(account);
}
}
输出 null 查看mock()源码可以看到里边ReturnsEmptyValues最终返回的null
如果想自定义其他的answer的方式
@RunWith(MockitoJUnitRunner.class)
public class MockByRunner {
@Test
public void testMock(){
AccountDao accountDao = mock(AccountDao.class, Mockito.RETURNS_SMART_NULLS);
Account account = accountDao.findAccount("x", "x");
System.out.println(account);
}
}
输出
SmartNull returned by this unstubbed method call on a mock:
accountDao.findAccount("x", "x");
2.MockitoAnnotations.initMocks(this) & @Mock
// An highlighted block
public class MockByAnnocation {
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
// @Mock
@Mock(answer = Answers.RETURNS_SMART_NULLS)//自定义answer方式
private AccountDao accountDao;
@Test
public void testMock(){
Account account = accountDao.findAccount("x", "x");
System.out.println(account);
}
}
3.MockitoRule mockitoRule = MockitoJUnit.rule()
// An highlighted block
public class MockByRule {
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Mock
private AccountDao accountDao;
@Test
public void testMock(){
//AccountDao accountDao = mock(AccountDao.class);//使用@Mock注解或者mock()都可以
Account account = accountDao.findAccount("x", "x");
System.out.println(account);
}
}
输出 null
通常情况使用前两种比较多
DeepMock
public class Order {
public void foo(){
//模拟异常
throw new RuntimeException();
}
}
public class OrderService {
public Order get(){
//模拟异常
throw new RuntimeException();
}
}
public class DeepMockTest {
@Mock
private OrderService orderService;
@Mock
private Order order;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void testMock(){
Order order = orderService.get();
//orderService.get()返回的是null,在调用方法肯定异常
order.foo();
}
}
运行异常 java.lang.NullPointerException
如何使返回的order不为null呢?就需要对orderService.get() 进行 stubbing操作
通俗点讲就是造数据,对入参和出参预期值的预设
// An highlighted block
public class DeepMockTest {
@Mock
private OrderService orderService;
@Mock
private Order order;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void testMock(){
when(orderService.get()).thenReturn(order);
Order o1 = orderService.get();
o1.foo();
}
}
有没有一种方式 在Mock orderService的时候顺便把get出来的order也mock了
// Answers.RETURNS_DEEP_STUBS
public class DeepMockTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)//deepmock 看情况使用,不一定是期望的返回值
private OrderService orderService;
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void testMock(){
orderService.get().foo();
}
}
Mockito Stubbing的使用
Stubbing就是对入参和出参预期值的预设
public class StubbingService {
public int getInt(){
System.out.println("------getI()-----");
return 10;
}
public String getSomething(){
throw new RuntimeException();
}
}
@RunWith(MockitoJUnitRunner.class)
public class StubbingTest {
private List<String> list;
@Before
public void init(){
this.list =mock(ArrayList.class);
}
@Test
public void howToUserStubbing(){
//list本身是空的 get(0)的时候拟定一个返回值"first"
when(list.get(0)).thenReturn("first");
//断言list.get(0)是否等于"first"
assertThat(list.get(0),equalTo("first"));
//拟定get()任意值的时候抛异常
when(list.get(anyInt())).thenThrow(new RuntimeException());
try{
list.get(0);
fail();
}catch (Exception e){
//断言e 是否是RuntimeException
assertThat(e,instanceOf(RuntimeException.class));
}
}
//模拟void方法的stubbing
@Test
public void howToUserStubbingVoidMethod(){
//执行list.clear()后不做其他处理
doNothing().when(list).clear();
list.clear();
//判断list.clear方法有没有执行? 执行了几次?
verify(list,times(1)).clear();
//执行clear()方法之后 抛异常
doThrow(RuntimeException.class).when(list).clear();
try{
list.clear();
fail();
}catch (Exception e){
//断言是否抛了RuntimeException
assertThat(e,instanceOf(RuntimeException.class));
}
}
//模拟有返回值的 stubbing
@Test
public void stubbingDoReturn(){
//方式1
when(list.get(0)).thenReturn("first");
//方式2
doReturn("second").when(list).get(1);
assertThat(list.get(0),equalTo("first"));
assertThat(list.get(1),equalTo("second"));
}
//模拟迭代的stubbing
@Test
public void interateStubbing(){
when(list.size()).thenReturn(1,2,3,4);
//断言第一次调用是否返回1
assertThat(list.size(),equalTo(1));
//断言第二次调用是否返回2
assertThat(list.size(),equalTo(2));
assertThat(list.size(),equalTo(3));
assertThat(list.size(),equalTo(4));
}
//模拟每次循环后进行一些处理
@Test
public void stubbingWithAnswer(){
//普通@Override写法
// when(list.get(anyInt())).thenAnswer(new Answer<String>() {
// @Override
// public String answer(InvocationOnMock invocationOnMock) throws Throwable {
// Integer index = invocationOnMock.getArgumentAt(0, Integer.class);
// //todosomething
// return String.valueOf(index * 10);
// }
// });
//lombda的写法
when(list.get(anyInt())).thenAnswer(invocationOnMock ->{
Integer index = invocationOnMock.getArgumentAt(0, Integer.class);
//todosomething
return String.valueOf(index * 10);
});
//断言循环处理的结果是否是预期值
assertThat(list.get(0),equalTo("0"));
assertThat(list.get(10),equalTo("100"));
}
//模拟部分调用实际的逻辑
@Test
public void stubbingWithRealCall(){
//mock一个 StubbingService
StubbingService service = mock(StubbingService.class);
//mock出的是CGLIB生成的代理,并不是实际的StubbingService
System.out.println(service.getClass());
service.getSomething();//并不会报错
when(service.getSomething()).thenReturn("Allen");
assertThat(service.getSomething(),equalTo("Allen"));
//thenCallRealMethod() 来调用实际的业务逻辑
when(service.getInt()).thenCallRealMethod();
assertThat(service.getInt(),equalTo(10));
}
@After
public void destory(){
//把stubbing动作都销毁掉
reset(this.list);
}
}
Stubbing的几种使用方式
when().thenReturn()
when().thenThrow()
doNothing().when(list).clear();
doThrow(RuntimeException.class).when(list).clear();
doReturn("second").when(list).get(1);
when(list.size()).thenReturn(1,2,3,4);//迭代
when(list.get(anyInt())).thenAnswer();//迭代
when(service.getInt()).thenCallRealMethod();
断言的方式
assertThat(xxx,equalTo("xxx"));
verify(list,times(1)).clear();
Mockito Spying的使用
其实就是先创建一个真实对象然后通过spy创建一个代理对象进行操作
对部分方法进行mock,进行stubbing的操作
@RunWith(MockitoJUnitRunner.class)
public class SpyingTest {
@Test
public void testSpy(){
List<String> realList = new ArrayList<>();
//mock一个list 就相当于是realList的代理
List<String> list = spy(realList);
list.add("Mockito");
list.add("Power");
System.out.println(list);
System.out.println(realList);
assertThat(list.get(0),equalTo("Mockito"));//成立 true
assertThat(list.get(1),equalTo("Power"));//true
assertThat(list.isEmpty(),equalTo(false));//true
//对list方法的值进行stubbing预设
when(list.isEmpty()).thenReturn(true);
when(list.size()).thenReturn(0);
assertThat(list.get(0),equalTo("Mockito"));//true
assertThat(list.get(1),equalTo("Power"));//true
//进行stubbing预设后会执行预设的方法
assertThat(list.isEmpty(),equalTo(true));//true
assertThat(list.size(),equalTo(0));//true
}
}
打印:
[Mockito, Power]
[]
也可以通过注解的方式
public class SpyingAnnotationTest {
@Spy
private List<String> list = new ArrayList<>();
@Before
public void init(){
MockitoAnnotations.initMocks(this);
}
@Test
public void spyingByAnnotationTest(){
list.add("Mockito");
list.add("Power");
assertThat(list.get(0),equalTo("Mockito"));
assertThat(list.get(1),equalTo("Power"));
assertThat(list.isEmpty(),equalTo(false));
when(list.isEmpty()).thenReturn(true);
when(list.size()).thenReturn(0);
assertThat(list.get(0),equalTo("Mockito"));
assertThat(list.get(1),equalTo("Power"));
assertThat(list.isEmpty(),equalTo(true));
assertThat(list.size(),equalTo(0));
}
}
Mockito Argument Matcher
Mockito通过equals()方法,来对方法参数进行验证。但有时我们需要更加灵活的参数需求,比如,匹配任何的String类型的参数等等。参数匹配器就是一个能够满足这些需求的工具。
eq() :eq(0) 等价于 0
isA():isA(xxx.class)必须是xxx的实例
any():满足语法的任何类型都可以
package org.example.mockito.argument;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.*;
public class ArgumentsMatcherTest {
@Test
public void baseTest(){
List<Integer> list = mock(ArrayList.class);
// eq(0) 等价于 0
when(list.get(eq(0))).thenReturn(100);
assertThat(list.get(0),equalTo(100));
assertThat(list.get(1),nullValue());
}
@Test
public void testComplex(){
Foo foo = mock(Foo.class);
//isA(xxx.class)必须是xxx的实例
when(foo.function(isA(Parent.class))).thenReturn(100);
int result = foo.function(new Child1());
assertThat(result,equalTo(100));
result = foo.function(new Child2());
assertThat(result,equalTo(100));
//重置foo
reset(foo);
//any() 满足语法的任何类型都可以
when(foo.function(any(Child1.class))).thenReturn(100);
result = foo.function(new Child2());
assertThat(result,equalTo(100));
}
static class Foo{
int function(Parent p){
return p.work();
}
}
interface Parent{
int work();
}
class Child1 implements Parent{
@Override
public int work() {
throw new RuntimeException();
}
}
class Child2 implements Parent{
@Override
public int work() {
throw new RuntimeException();
}
}
}
Wildcard Argument Matcher
Mockito框架中的Matchers类内建了很多参数匹配器,而我们常用的Mockito对象便是继承自Matchers。这些内建的参数匹配器如,anyInt()匹配任何int类型参数,anyString()匹配任何字符串,anySet()匹配任何Set等。下面通过例子来说明如何使用内建的参数匹配器:
注意:同一个语句多个参数必须统一,如果使用wildcard那么所有的参数必须都使用wildcard,要么就都不使用,否则报错
package org.example.mockito.argument;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.Serializable;
import java.util.Collections;
import java.util.List;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class WildcardArgumentMatcher {
@Mock
private SimpleService simpleService;
@Test
public void wildcardMethodTest(){
when(simpleService.method1(anyInt(),anyString(),anyCollection(),isA(Serializable.class))).thenReturn(100);
int result = simpleService.method1(1, "Allen", Collections.emptyList(), "Mockito");
assertThat(result,equalTo(100));
result = simpleService.method1(1, "Zhu", Collections.emptySet(), "MockitoForXXXX");
assertThat(result,equalTo(100));
}
@Test
public void testWilcardSpcMethod1(){
//stubbing
// when(simpleService.method1(anyInt(),anyString(),anyCollection(),isA(Serializable.class))).thenReturn(-1);
when(simpleService.method1(anyInt(),eq("Allen"),anyCollection(),isA(Serializable.class))).thenReturn(100);
when(simpleService.method1(anyInt(),eq("Zhu"),anyCollection(),isA(Serializable.class))).thenReturn(200);
//正常匹配到
int ressult = simpleService.method1(1,"Allen", Collections.emptyList(),"Mockito");
assertThat(ressult,equalTo(100));
ressult = simpleService.method1(1,"Zhu", Collections.emptyList(),"Mockito");
assertThat(ressult,equalTo(200));
//匹配不到
ressult = simpleService.method1(1,"ZhuXXXXX", Collections.emptyList(),"Mockito");
assertThat(ressult,equalTo(0));
// assertThat(ressult,equalTo(-1));
}
@Test
public void testWilcardSpcMethod2(){
//stubbing
List<Object> emptyList = Collections.emptyList();
doNothing().when(simpleService).method2(anyInt(),anyString(),anyCollection(),isA(Serializable.class));
simpleService.method2(1,"Allen",emptyList,"Mockito");
//
verify(simpleService,times(1)).method2(1,"Allen",emptyList,"Mockito");
verify(simpleService,times(1)).method2(anyInt(),eq("Allen"),anyCollection(),isA(Serializable.class));
}
@After
public void destroy(){
reset(simpleService);
}
}
Hamcrest Matcher一些简单使用
assertThat()的使用
package org.example.mockito.matcher;
import org.junit.Test;
import java.util.stream.Stream;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.MatcherAssert.*;
public class AssertMatcherTest {
@Test
public void test(){
int i = 10;
assertThat(i,equalTo(10));
assertThat(i,not(equalTo(20)));
assertThat(i,is(10));
assertThat(i,is(not(20)));
}
@Test
public void test2(){
double price = 10.23;
// or
assertThat(price,either(equalTo(10.23)).or(equalTo(20.23)));
// and
assertThat(price,both(equalTo(10.23)).and(not(equalTo(20.23))));
// 任意一个满足
assertThat(price,anyOf(is(10.23),is(10),is(10),not(23.23)));
// 所有的都要满足
assertThat(price,allOf(is(10.23),not(is(20.23)),not(is(30.23))));
// 任意一个满足
assertThat(Stream.of(1,2,3).anyMatch( i -> i > 2),equalTo(true));
// 所有的都要满足
assertThat(Stream.of(1,2,3).allMatch( i -> i > 0),equalTo(true));
}
@Test
public void test3(){
double price = 10.23;
// 断言失败输出语句
assertThat("the double value assertThat failed.",price,both(equalTo(10.23)).and(equalTo(20.23)));
}
}
自定义Matcher
package org.example.mockito.matcher;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
public class GreaterThan<T extends Number> extends BaseMatcher {
private final T value;
private final Compare<T> COMPARE;
public GreaterThan(T value, boolean greate) {
this.COMPARE = new DefaultNumberCompare<>(greate);
this.value = value;
}
@Override
public boolean matches(Object actual) {
return this.COMPARE.compare(value,(T)actual);
}
public static <T extends Number> GreaterThan<T> gt(T value){
return new GreaterThan<>(value,true);
}
public static <T extends Number> GreaterThan<T> lt(T value){
return new GreaterThan<>(value,false);
}
private interface Compare<T extends Number>{
boolean compare(T expected,T actual);
}
private static class DefaultNumberCompare<T extends Number> implements Compare<T>{
private final Boolean greate;
public DefaultNumberCompare(Boolean greate) {
this.greate = greate;
}
@Override
public boolean compare(T expected, T actual) {
Class<?> calzz = actual.getClass();
if(calzz == Integer.class){
return greate ? (Integer) actual > (Integer) expected : (Integer) actual < (Integer) expected;
}else if (calzz == Byte.class){
return greate ? (Byte) actual > (Byte) expected:(Byte) actual < (Byte) expected;
}else if (calzz == Short.class){
return greate ? (Short) actual > (Short) expected:(Short) actual < (Short) expected;
}else if (calzz == Long.class){
return greate ? (Long) actual > (Long) expected :(Long) actual < (Long) expected;
}else if (calzz == Double.class){
return greate ? (Double) actual > (Double) expected:(Double) actual < (Double) expected;
}else if (calzz == Float.class){
return greate ? (Float) actual > (Float) expected:(Float) actual < (Float) expected;
}else {
throw new AssertionError("The number type "+calzz+" not support.");
}
}
}
@Override
public void describeTo(Description description) {
description.appendText("compare two num failed.");
}
}
package org.example.mockito.matcher;
import org.junit.Test;
import static org.hamcrest.MatcherAssert.*;
import static org.example.mockito.matcher.GreaterThan.gt;
public class GreateTest {
@Test
public void test(){
assertThat(10,gt(5));
}
}