亲宝软件园·资讯

展开

mockito测试教程

MFine 人气:0

前言

首先声明笔者并不是一个TDD开发的拥趸,我倾向于实用主义。TDD有他的用武之地,但不是银弹,同理BDD(行为驱动开发)也是如此。笔者坚持写测试的原因只是为了更加方便的重构,当笔者意识到一个模块会被大范围使用且会面临多次迭代的时候,笔者就会认证写测试,并且每次出bug都会用测试先复现。如果一个项目只是一次性的demo那些个啥的测试,一把梭哈得了。

什么是TDD

TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。

为什么要使用mockito

Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API. Mockito doesn’t give you hangover because the tests are very readable and they produce clean verification errors.

Mockito 是非常不错框架。它使您可以使用干净简单的 API 编写漂亮的测试。 Mockito 不会给你带来宿醉,因为测试非常易读并且会产生干净的验证错误

在开发程序的时候我们需要测试的类不可能都是简单的类,很多复杂的逻辑往往需要多个前置条件才能测试到。如果为了测试去满足这些前置条件未免显得太繁琐。比如有的逻辑需要进行HTTP请求某个特定服务,我不想在测试的时候去单独启动这个服务。这时候我们就可以mock这个http请求,让其返回一个特定值,以此简化测试流程。

如何使用mockito

前期准备

引包

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>${mockito.version}</version>
    <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-inline -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>${mockito.version}</version>
    <scope>test</scope>
</dependency>

静态导入

import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
import static org.mockito.Mockito.*;

包装你要模拟的类

使用mock方法

LinkedList mockedList = mock(LinkedList.class);

使用spy方法

PDStateMachineNode mock = spy(new PDStateMachineNode());

mockspy的区别

mock方法和spy方法都可以对对象进行mock。但是前者是接管了对象的全部方法,而后者只是将有桩实现(stubbing)的调用进行mock,其余方法仍然是实际调用。大家先这样理解后面会具体举例子。

什么插桩

其实就是对要模拟方法进行包装,设定返回值或者抛异常之类,直接看官方例子。这里就是对get(0)get(1) 进行插桩了

 //You can mock concrete classes, not just interfaces
 LinkedList mockedList = mock(LinkedList.class);
 //插桩
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(1)).thenThrow(new RuntimeException());
 //following prints "first"
 System.out.println(mockedList.get(0));
 //following throws runtime exception
 System.out.println(mockedList.get(1));
 //following prints "null" because get(999) was not stubbed
 System.out.println(mockedList.get(999));
 //Although it is possible to verify a stubbed invocation, usually it's just redundant
 //If your code cares what get(0) returns, then something else breaks (often even before verify() gets executed).
 //If your code doesn't care what get(0) returns, then it should not be stubbed.
 verify(mockedList).get(0);

验证行为

可以用来验证某个方法是否调用了

这里使用了官网例子,大致用途就是你可以用它来验证某个方法被调用了没有。可以很明显在的看到他在验证是否调用了add和clear

 //Let's import Mockito statically so that the code looks clearer
 import static org.mockito.Mockito.*;
 //mock creation
 List mockedList = mock(List.class);
 //using mock object
 mockedList.add("one");
 mockedList.clear();
 //verification
 verify(mockedList).add("one");
 verify(mockedList).clear();

下面看看我自己项目里面得例子。这里可以发现我在传入参数得时候直接是传入了any(),这也是mockito提供得,与此对应得还有anyString(),anyInt()等等。

大致意思就是验证runAviatorScript这个方法被调用的了应该有一次。

@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
    PDStateMachineNode mock = spy(PDStateMachineNode.class);
    mock.onEntry(any());
    // 默认第二个参数就是times(1),因此这里可以不写
    verify(mock,times(1)).runAviatorScript(any(),any(),anyString());
}

参数匹配

方便得模拟一次方法得参数

直接上官方例子,上述代码也有使用

//stubbing using built-in anyInt() argument matcher
 when(mockedList.get(anyInt())).thenReturn("element");
 //stubbing using custom matcher (let's say isValid() returns your own matcher implementation):
 when(mockedList.contains(argThat(isValid()))).thenReturn(true);
 //following prints "element"
 System.out.println(mockedList.get(999));
 //you can also verify using an argument matcher
 verify(mockedList).get(anyInt());
 //argument matchers can also be written as Java 8 Lambdas
 verify(mockedList).add(argThat(someString -> someString.length() > 5));

如果你使用的参数匹配器,那么所有参数都必须提供参数匹配器,否则会抛异常。

//正确   
verify(mock).someMethod(anyInt(), anyString(), eq("third argument"));
//错误
verify(mock).someMethod(anyInt(), anyString(), "third argument");

验证调用次数

就是验证某个方法调用了多少次

 //using mock
 mockedList.add("once");
 mockedList.add("twice");
 mockedList.add("twice");
 mockedList.add("three times");
 mockedList.add("three times");
 mockedList.add("three times");
 //following two verifications work exactly the same - times(1) is used by default
 verify(mockedList).add("once");
 verify(mockedList, times(1)).add("once");
 //exact number of invocations verification
 verify(mockedList, times(2)).add("twice");
 verify(mockedList, times(3)).add("three times");
 //verification using never(). never() is an alias to times(0)
 verify(mockedList, never()).add("never happened");
 //verification using atLeast()/atMost()
 verify(mockedList, atMostOnce()).add("once");
 verify(mockedList, atLeastOnce()).add("three times");
 verify(mockedList, atLeast(2)).add("three times");
 verify(mockedList, atMost(5)).add("three times");

笔者项目中得例子

@Test
void testDELETE() {
    Assertions.assertTimeout(Duration.ofSeconds(10), () -> {
        mockStatic.when(() -> HttpRequest.delete("test").execute().body()).thenReturn("success");
        String execute = (String) AviatorEvaluator.execute("return DELETE("test");");
        Assertions.assertTrue(execute.contains("success"));
        //在这里
        mockStatic.verify(() -> HttpRequest.delete(anyString()), times(2));
    });
}

模拟void方法

项目中的例子

当调用onExit的时候啥也不干

@Test
void onExit() throws NodeExecuteTimeoutException {
    StateMachineInterpreter interpreter = new StateMachineInterpreter();
    StateMachineNode spy = spy(new StateMachineNode());
    //啥也不干
    doNothing().when(spy).onExit(any());
}

验证调用顺序

有时候在需要验证某个方法内部调用其他方法的顺序。笔者例子如下:

这段测试意思就是模拟 PDStateMachineNode这个类调用verify()方法得逻辑。分两种情况

onCheck方法返回true(doReturn(true).when(mock).onCheck(any())

此种情况下,方法调用顺序应为:onEntry,onCheck,onMatch,onExit

onCheck返回false(doReturn(false).when(mock).onCheck(any())

此种情况下,方法调用顺序应为:onEntry,onCheck,onFail,onExit

PDStateMachineNode mock = spy(new PDStateMachineNode());
doReturn(true).when(mock).onCheck(any());
mock.verify(any());
InOrder inOrder = inOrder(mock);
inOrder.verify(mock).onEntry(any());
inOrder.verify(mock).onCheck(any());
inOrder.verify(mock).onMatch(any());
inOrder.verify(mock).onExit(any());
doReturn(false).when(mock).onCheck(any());
mock.verify(any());
InOrder inOrder2 = inOrder(mock);
inOrder2.verify(mock).onEntry(any());
inOrder2.verify(mock).onCheck(any());
inOrder2.verify(mock).onFail(any());
inOrder2.verify(mock).onExit(any());

doReturn()|doThrow()| doAnswer()|doNothing()|doCallRealMethod()

官方建议大部分情况下你应该使用when(),二者区别后文再说。

doReturn()

List list = new LinkedList();
List spy = spy(list);
//Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
when(spy.get(0)).thenReturn("foo", "bar", "qix");
//You have to use doReturn() for stubbing:
doReturn("foo", "bar", "qix").when(spy).get(0);
   List list = new LinkedList();
   List spy = spy(list);
   //Impossible: real method is called so spy.get(0) throws IndexOutOfBoundsException (the list is yet empty)
   when(spy.get(0)).thenReturn("foo", "bar", "qix");
   //You have to use doReturn() for stubbing:
   doReturn("foo", "bar", "qix").when(spy).get(0);

doThrow()

doThrow(new RuntimeException()).when(mock).someVoidMethod();
doThrow(RuntimeException.class).when(mock).someVoidMethod();

doAnswer()

doAnswer(new Answer() {
    public Object answer(InvocationOnMock invocation) {
        Object[] args = invocation.getArguments();
        Mock mock = invocation.getMock();
        return null;
    }})
    .when(mock).someMethod();

doNothing()

doNothing().
    doThrow(new RuntimeException())
    .when(mock).someVoidMethod();
//does nothing the first time:
mock.someVoidMethod();
//throws RuntimeException the next time:
mock.someVoidMethod();

doCallRealMethod()

Foo mock = mock(Foo.class);
doCallRealMethod().when(mock).someVoidMethod();
// this will call the real implementation of Foo.someVoidMethod()
mock.someVoidMethod();

笔者项目中的使用的例子

@Test
void step() throws NodeExecuteTimeoutException, NextNodesNotExistException {
    PDStateMachineNode spy = spy(new PDStateMachineNode());
    PDStateMachineNode node = new PDStateMachineNode();
    PDStateMachineNode subSpy = spy(node);
    doReturn(true).when(subSpy).verify(any());
    doReturn(List.of(subSpy)).when(spy).getNextNodes();
    PDStateMachineNode step = spy.step(any(PointData.class));
    Assertions.assertEquals(subSpy, step);
    when(spy.getNextNodes()).thenReturn(new ArrayList<>());
    doReturn(true).when(spy).isTerminalNode();
    Assertions.assertThrows(NextNodesNotExistException.class, () -> spy.step(any(PointData.class)));
    doReturn(new ArrayList<>()).when(spy).getNextNodes();
    doReturn(false).when(spy).isTerminalNode();
    Assertions.assertEquals(spy, spy.step(any(PointData.class)));
}

静态方法模拟

静态有返回值且存在链式调用

直接看笔者项目中的例子,注意这里下面的例子有些许不同,RETURNS_DEEP_STUBS其实是用来进行嵌套模拟的。因为HttpRequest.get("test").execute().body() 是一个链式的调用,实际上涉及了多个类的模拟。如果你没有这个需求就可以不加。

@BeforeAll
void init() {
    mockStatic = mockStatic(HttpRequest.class, RETURNS_DEEP_STUBS);
}
@Test
void testGET() {
    Assertions.assertTimeout(Duration.ofSeconds(10), () -> {
        mockStatic.when(() -> HttpRequest.get("test").execute().body()).thenReturn("success");
        String execute = (String) AviatorEvaluator.execute("return GET("test");");
        Assertions.assertTrue(execute.contains("success"));
        mockStatic.verify(() -> HttpRequest.get(anyString()), times(2));
    });
}

静态无返回值

someMock.when(() -> Files.delete(fileToDelete)).thenAnswer((Answer<Void>) invocation -> null);
// 也可以是下面这个
// someMock.when(() -> Files.delete(fileToDelete)).thenAnswer(Answers.RETURNS_DEFAULTS);

进阶

mock和spy的区别

mock方法和spy方法都可以对对象进行mock。但是前者是接管了对象的全部方法,而后者只是将有桩实现(stubbing)的调用进行mock,其余方法仍然是实际调用。

使用mock

PDStateMachineNode mock = mock(PDStateMachineNode.class);
mock.onEntry(any());
verify(mock, times(1)).runAviatorScript(any(), any(), anyString());

抛错如下,意思就是runAviatorScript没有被调用,因此验证失败。实际上笔者在onEntry内部是调用了runAviatorScript方法的

Wanted but not invoked:
pDStateMachineNode.runAviatorScript(
    <any>,
    <any>,
    <any string>
);
-> at core.state.GenericStateMachineNode.runAviatorScript(GenericStateMachineNode.java:78)

使用spy,则无任务错误。

@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
    PDStateMachineNode mock = spy(PDStateMachineNode.class);
    mock.onEntry(any());
    verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
}

从上述对比就可以理解mock和spy的区别,对于未指定mock的方法,spy默认会调用真实的方法,有返回值的返回真实的返回值,而mock默认不执行,有返回值的,默认返回null。具体细节笔者也没有深究比如实际上mock也能做到类似psy的效果

when(...).thenReturn(...)和doReturn(...).when(...)的区别

● when(...) thenReturn(...)会调用真实的方法,如果你不想调用真实的方法而是想要mock的话,就不要使用这个方法。

● doReturn(...) when(...) 不会调用真实方法

因此针对区别一般情况下如果时第三方库得代码在需要测试得方法则可以使用 do...return进行略过,自己调用自己得方法则建议使用 when...return。但是有时候调用得方法需要一些特殊得环境才能起作用,那么也能使用 do..return,亦或者被调用得方法已经测试过了也可以使用 do..return。下面看二者区别得例子。

例子:

@Override
public void onEntry(T event) throws NodeExecuteTimeoutException {
    System.out.println("hello");
    this.runAviatorScript(this.onEntry, event, "onEntry");
}

测试when..return...:

    @Test
    void testOnEntry2() throws NodeExecuteTimeoutException {
        PDStateMachineNode mock = spy(PDStateMachineNode.class);
        when(mock.onCheck(any())).thenReturn(true);
        mock.onEntry(any());
        verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
    }

结果可以看到输出得hello

测试do...return...

@Test
void testOnEntry2() throws NodeExecuteTimeoutException {
    PDStateMachineNode mock = spy(PDStateMachineNode.class);
    doNothing().when(mock).onEntry(any());
    mock.onEntry(any());
    verify(mock, times(1)).runAviatorScript(any(), any(), anyString());
}

结果可以看到不仅没输出还报错了,为什么呢?因为 do..return实际上不执行包装得方法,也就没有执行onEntry方法,自然里面 runAviatorScript也就没有执行,因此就会导致验证错误。

BDDMockito(行为驱动测试)

什么是BDD

行为驱动开发(英语:Behavior-driven development,缩写BDD)是一种敏捷软件开发的技术,它鼓励软件项目中的开发者、QA和非技术人员或商业参与者之间的协作。BDD最初是由Dan North在2003年命名,它包括验收测试和客户测试驱动等的极限编程的实践,作为对测试驱动开发的回应。在过去数年里,它得到了很大的发展。行为驱动测试的开发风格使用//given//when//then 作为测试方法的基本部分。

其实还是比较简单的,粗浅的理解就是换了几个API。

举个例子

直接看几个官方的例子

 import static org.mockito.BDDMockito.*;
 Seller seller = mock(Seller.class);
 Shop shop = new Shop(seller);
 public void shouldBuyBread() throws Exception {
   //given
   given(seller.askForBread()).willReturn(new Bread());
   //when
   Goods goods = shop.buyBread();
   //then
   assertThat(goods, containBread());
 }

如何模拟异常

可以发现willThrow就像之前的doThrow差不多

   //given
   willThrow(new RuntimeException("boo")).given(mock).foo();
   //when
   Result result = systemUnderTest.perform();
   //then
   assertEquals(failure, result);

验证调用次数

person.ride(bike);
person.ride(bike);
then(person).should(times(2)).ride(bike);
then(person).shouldHaveNoMoreInteractions();
then(police).shouldHaveZeroInteractions();

验证调用顺序

   InOrder inOrder = inOrder(person);
   person.drive(car);
   person.ride(bike);
   person.ride(bike);
   then(person).should(inOrder).drive(car);
   then(person).should(inOrder, times(2)).ride(bike);

实战中使用

这里不仅模拟了方法的返回值,还模拟了springbootcontroller的调用

@Test
void shouldNotListRoles() throws Exception {
    given(roleService.findAll()).willReturn(new ArrayList<>());
    ResultActions actions = this.mvc.perform(get("/api/role/getRoles"));
    actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8");
    actions.andDo(print()).andExpect(jsonPath("$.data.length()").value(Matchers.is(0)));
}
@Test
void shouldCreateRole() throws Exception {
    objectMapper.registerModule(new JavaTimeModule());
    objectMapper.disable(SerializationFeature.WRITE_DATE_KEYS_AS_TIMESTAMPS);
    Role role = new Role(null, "mfine",
                         LocalDateTime.now(), "", 0, null, "admin");
    // 这里 也使用了参数匹配器
    given(roleService.insertSelective(BDDMockito.any())).willReturn(1);
    ResultActions actions = this.mvc.perform(post("/api/role/createRole").content(objectMapper.writeValueAsString(role))
                                             .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON));
    actions.andExpect(status().isOk()).andReturn().getResponse().setCharacterEncoding("UTF-8");
    actions.andExpect(ResultMatcher.matchAll(result -> {
        Assert.assertTrue(result.getResponse().getContentAsString().contains("success"));
    }));
}

总结

完整的测试时重构得底气,当没有测试得时候一切重构都是扯淡。程序出bug之后第一件事应该是复现bug,增补测试然后再是修复bug,如果是线上紧急情况那也应该在时候补充测试。但是测试也不是银弹,所谓的TDD和BDD也要看情况使用,没有万能方案只有适合方法。

加载全部内容

相关教程
猜你喜欢
用户评论