前言
因为单元测试的需要,经常会需要mock掉一些外部引用或者数据库接入等内容。接触过JMockit、Mockito、PowerMock等工具,但最终还是选择了Mockito。
相较之下。JMockit经常被认为是比较全面的,除了常见mock掉类暴露的方法,类内的私有方法,构造方法,静态方法,甚至用final修饰的方法都能很好的mock掉。
而Mockito相对了其他两种,其语法十分的易于理解,可读性很好。这也是我喜欢Mockito的原因,而PowerMock则类似于Mockito的超集,但Maven引入PowerMock的时候,也就自动引入了Mockito。
Mockito常被人认为功能不够强大,对于静态方法,构造方法等内容,需要借助PowerMock才能执行。但那已经是几年前的事情了。2016年推出了Mockito2.x,已经解决了这些问题。
Mockito的源码中注释十分全面。其org.mockito.Mockito类中注释了大量相关内容,仔细阅读对Mockito的使用很有帮助。
以下是个人对Mockito学习的一些简易总结,也是简单记录以供后续有需查阅。
1、mock公有方法
public class AppleTest { @Test public void test_mock_public_method() { Apple apple = mock(Apple.class); when(apple.getColor()).thenReturn("green"); Assertions.assertEquals("green", apple.getColor()); } } class Apple { public String getColor() { return "red"; } }
语法比较简单,创建mock对象,然后类似when…thenReturn…语法修改方法的返回,也可使用thenThrow抛出异常。
2、mock静态方法
public class AppleTest { @Test public void test_mock_static_method() { try (MockedStatic<Apple> mockedStatic = Mockito.mockStatic(Apple.class)) { when(Apple.getColor()).thenReturn("green"); Assertions.assertEquals("green", Apple.getColor()); } } } class Apple { public static String getColor() { return "red"; } }
官方建议是通过try-resouce语句包裹内容。因为采用的是ThreadLocal保存对静态方法的控制,try-resouce能保证mock使用结束后调用MockStatic.close()清除。也可调用Mockito.framework().clearInlineMocks(); 一次性清除全部Mock对象。
3、Mock构造方法
public class AppleTest { @Test public void test_mock_construction_method() { try (MockedConstruction<Banana> mockConstruction = mockConstructionWithAnswer(Banana.class, invocation -> "green")) { Apple apple = new Apple(); Assertions.assertEquals("green", apple.getColor()); } } } class Apple { public String getColor() { return new Banana().getColor(); } } class Banana { public String getColor() { return "yellow"; } }
重要:普通的依赖注入,都是比较好mock的。但编写的代码里若是new了其他对象,就会比较难mock,mock构造函数可以解决这个问题,如果mock构造方法的对象行为比较复杂的话,也可以先提前构造mock对象来处理。
Banana mockBanana = mock(Banana.class); when(mockBanana.getColor()).thenReturn("green"); try(MockedConstruction<Banana>mockConstruction = mockConstructionWithAnswer(Banana.class, invocation ->invocation.getMethod().invoke(mockBanana,invocation.getArguments()))){ Apple apple = new Apple(); Assertions.assertEquals("green",apple.getColor()); }
另一个mock构造函数的例子
final Foo foo = mock(Foo.class); MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { if (invocation.getMethod().equals(Foo.class.getMethod("getFooName"))) { return "mocked value"; } return foo; } });
final Foo foo = mock(Foo.class); MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { // 当mock对象的方法被调用时,会进入到这个方法里面。通过判断方法名字,返回mock的value if (invocation.getMethod().getName().equals("getFooName"))) { return "mocked value"; } return foo; } });
// mock当调用对象某个方法时出异常 final Foo foo = mock(Foo.class); MockedConstruction<Foo> mockedConstruction = mockConstructionWithAnswer(Foo.class, new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { // 当mock对象的方法被调用时,会进入到这个方法里面。通过判断方法名字,返回mock的value if (invocation.getMethod().getName().equals("getFooName"))) { throw new java.lang.RuntimeException(); } return foo; } });
4、Mock私有方法
这个其实是不支持的。截止当前最新的3.11.1版本。Mockito本身并不支持mock私有方法。其官方是有说明原因的,其最后一点:
Finally... Mocking private methods is a hint that there is something wrong with Object Oriented understanding. In OO you want objects (or roles) to collaborate, not methods. Forget about pascal & procedural code.Think in objects. https://github.com/mockito/mockito/wiki/Mockito-And-Private-Methods
这一点还是比较认同的,如果需要mock私有方法,那大概率程序在面向对象的设计上存在问题。但其实这很不好说,说不定过两个版本Mockito就打脸了~
5、使用@Mock/@InjectMocks注解
Mockito也提供了注解的方式来实现对依赖的打桩以及注入,也就是@Mock和@InjectMocks注解。
import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; public class PortfolioTest { @InjectMocks Portfolio portfolio; @Mock StockService stockService; @BeforeEach public void setUp() { MockitoAnnotations.initMocks(this); } }
- @Mock注解:Mockito 通过 @mock 注解来创建 mock 对象,可以用来代替Mockito.mock()方法。
- @InjectMocks:创建一个实例,并将@Mock(或@Spy)注解创建的mock注入到用该实例中。
在使用了这两个注解之后,setup()方法也发生了变化。额外增加了以下这样一行代码。
public void setUp() { MockitoAnnotations.initMocks(this); }
// 上面这个方法其实等价于这个 public void setUp() { portfolio = new Portfolio(); stockService = mock(StockService.class); portfolio.setStockService(stockService); }
也就是说,通过@Mock/@InjectMocks 以及initMocks方法的配合,Mockito实现了
- @Mock将外部依赖StockService 进行了mock
- @InjectMocks通过调用Portfolio类的无参构造方法完成了portfolio的实例化,并通过Portfolio类提供的setStockService()方法,用setter注入的方式,将前述被mock的stockService注入进portfolio。
6. mockito怎么mock一个类的私有属性
@InjectMocks private UserServiceImpl userService; @Test public void test() throws NoSuchFieldException{ Field apiField = UserServiceImpl.class.getDeclaredField("username"); // 已经为UserServiceImpl类的username属性成功设置值:1 FieldSetter.setField(userService, apiField, "1"); }
补充说明
良好的架构设计下,掌握以上的内容,就可以写出覆盖率很高的测试案例了。但Mockito提供的不仅如此。
- 调用Mockito的mock方法,创建的对象,默认是对其所有方法,原方法返回为基本类型的,则返回默认值,否则返回null。
public class AppleTest { @Test public void test_mock_and_spy_method(){ Apple mockApple = mock(Apple.class); Apple spyApple = spy(Apple.class); Assertions.assertNull(mockApple.getColor()); Assertions.assertEquals("red",spyApple.getColor()); } } class Apple{ public String getColor(){ return "red"; } }
而spy方法则是对实际对象的mock,可以传入具体的对象,也可以如上代码传入类,当传入类时,则自动构造对象。对于没有mock操作的方法,spy出来的对象默认调用原方法。
- 测试案例结束后,经常需要校验mock对象是否被正常调起。在Mockito1.x的时候,经常会在代码调用结束后,使用verify检查。另一方面,代码中存在大量的并不会被使用的mock对象,可能是从其他地方复制过来的,也可能一开始构思错误,使得测试代码并不简洁。
- 但从Mockito2.1开始,mockito趋向严格。当使用
@ExtendWith(MockitoExtension.class)
或是Junit4的MockitoJunitRunner时,默认采用严格模式,当出现不必要的桩时,则抛出异常。
org.mockito.exceptions.misusing.UnnecessaryStubbingException: Unnecessarystubbings detected. Clean & maintainabletest code requires zero unnecessary code.
当然也可更改严格程度,但推荐还是应该保留测试代码的整洁性。
- 当然即使有严格的校验mock对象被调起过,但实际使用的时候,有时会有一些更具体的验证。Mockito也提供了一系列方法。主要是下面这些:
// 验证调用了1次getColor方法 verify(apple,times(1)).getColor(); // 验证至少调用了1次getColor方法 verify(apple,atLeastOnce()).getColor(); // 验证从来没有调用过getWeight方法 verify(apple,never()).getWeight(); // 验证调用getColor方法耗时不超过100ms verify(apple,timeout(100)).getColor();
补充例子:mockito测试final类/static方法/自己new的对象
先准备几个类,方便后面讲解:
public final class FinalSampleUtils { public static String foo() { return "aaa"; } public static String bar(String a) { return "bar:" + a; } }
这是一个final类,里面有2个static方法。
public class NewObject { public String haha() { return "haha"; } }
这是一个平淡无奇的类,没啥好说的。它俩的使用方式如下:
import org.springframework.stereotype.Service; @Service public class SampleServiceImpl implements SampleService { NewObject obj; public SampleServiceImpl() { obj = new NewObject(); } @Override public void helloWorld() { String foo = FinalSampleUtils.foo(); String bar = FinalSampleUtils.bar("test"); System.out.println("hello1:" + foo); System.out.println("hello2:" + bar); System.out.println("h:" + obj.haha()); } }
这是一个普通的@Service实现类,但有2个注意的地方:
- 里面用到的NewObject,并不是@Autowired之类由Spring注入的,而是自己new的
- helloWorld里,使用了final类的静态方法,以及obj的普通方法。
在3.4以下的低版本mockito中,如果想mock helloWorld方法是很困难的,但在高版本中功能有所加强,参考下面的代码:
import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.*; import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.ArgumentMatchers.any; @RunWith(MockitoJUnitRunner.class) public class SampleServiceImplTest { @Mock NewObject obj; @InjectMocks SampleServiceImpl sampleService; @Test public void testHelloWorld() { MockedStatic<FinalSampleUtils> mocked = Mockito.mockStatic(FinalSampleUtils.class); //mock不带参数的static方法 mocked.when(FinalSampleUtils::foo).thenReturn("bbb"); //mock带参数的static方法 mocked.when(() -> FinalSampleUtils.bar(any())).thenReturn("xxx"); //mock代码中自己new的实例及“该实例的方法” MockedConstruction<NewObject> newObjectMocked = Mockito.mockConstruction(NewObject.class); Mockito.when(obj.haha()).thenReturn("who am i ?"); sampleService.helloWorld(); } }
跑出来的效果如下:
hello1:bbb hello2:xxx h:who am i ?
从输出上看,不管是带参还是不带参的static方法,都成功mock,返回了mock后的值,而且自己new的对象,也同样mock成功了。
最后总结
项目中,有些函数需要处理某个服务的返回结果,而在对函数单元测试的时候,又不能启动那些服务,这里就可以利用Mockito工具,其中有如下三种注解:
- @InjectMocks:创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。
- @Mock:对函数的调用均执行mock(即虚假函数),不执行真正部分。
- @Spy:对函数的调用均执行真正部分。
以上就是Mockito的主要玩法。其特征就是就是写出来的代码可读性比较高,易于理解。即使是未接触过Mockito的人,也能很快的理解每行代码的含义。当然,更详细的功能,看官方文档是最好的选择。
在学习Mockito的过程中,对其他方面也有一些思考。
- 第一点是,以前就听说,Mockito功能上不支持静态方法,final方法等。即使现在去百度,或者谷歌搜索“如何用Mockito mock静态方法”,得到的结果也是让引入PowerMock,但其实这些功能在几年前已经在Mockito自身支持了。也许学习这些内容,官方文档是更好的选择。
- 第二点是,Mockito只是工具。其实用JMockit也好,EasyMock也好,都只是共同达到一个方便测试的目的。而最终想要的,是想要写出一个真正能“跑完测试脚本之后,就能很放心认为这次的修改没有问题”的代码。如何设计测试案例,这是关键所在。而另一方面,一个类是否易于测试,是否在整体架构设计之时,就已经决定好了呢?