单元测试程序员

[Java] [Unit Test] Mocking Frame

2018-12-28  本文已影响114人  丑小丫大笨蛋

转到Java之后我还没有系统地学习那些常用的mock框架,平时写代码都是模仿着别人的code东抄抄西抄抄,不会的再去stack overflow找找答案。最近发现很多新人都是这样写单元测试的,然后看看代码库,同一个包里用不同的框架的大有人在。我并不是说这样不好,但是这样很容易误导新人。我遇见过两次有人在写单元测试的时候,annotation用的是一个mock框架,在setup的时候又是用的另一个框架,然后纠结着怎么跑不通,各种错误呢!🤷‍♀️我自己摸爬滚打了一阵之后,发现写单元测试的时候最好的参考文档并不是别人的代码,而是官方的Tutorial。
于是我开了这个系列,打算讲讲大家常用的JMockit和Mockito等框架,也借此机会系统学习一遍。这里假设读者对单元测试的基本知识、AAA (Arrange, Act, Assert)的三段式以及什么是mock都了解,而且这里的mock框架与你所用的单元测试的框架Junit/TestNG没有关系。
对于每个框架,我会先介绍它的主要的annotation和功能;然后以自己经常写到的use cases举例说明用这个框架该怎么写。


Mockito

1. Mockito能mock什么?

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在StackOverflow上被评为“the best mock framework for Java”肯定是有一定的道理的。它最大的缺点可能是无法mock那些static和final class或者private方法,但是PowerMockito弥补了这些不足。而且在面向对象的设计中我们越来越少地使用那些静态的或面向过程的设计,所以绝大多数情况Mockito都能很方便地用起来。

2. Annotations

3. Mockito的套路

Mockito像其他的mock框架一样,提供了一套Behavior Driven Development的单元测试书写三段式:Given, When, Then。

//Given
given(calcService.add(20.0,10.0)).willReturn(30.0);

//when
double result = calcService.add(20.0,10.0);

//then
Assert.assertEquals(result,30.0,0);

所以,用Mockito写单元测试大概是下面这个流程。
0)创建mock
创建mock的方式有两种,一种是利用@Mock标注,另一种则是直接用mock()静态方法calcService = mock(CalculatorService.class);
1)添加behavior
Mockito有好几种添加behavior的方式,乍一看有点儿眼花缭乱,其实主要是分为两类。

when(object.method()).thenReturn(value);
given(object.method()).willThrow(exception);
doThrow(new RuntimeException()).when(mockedList).clear();

2)执行测试
3)验证behavior
当验证behavior的时候我们一般验证指定的方法有没有被invoke,被调用了多少次,还可以capture被invoke时的参数值,用于做进一步的assert。

verify(calcService, times(1)).add(10.0, 20.0);
verify(calcService, never()).multiply(10.0,20.0);

对于确定的次数,一般用times(x)就可以了,比如never()也可以写times(0)。但是对于一些不确定但是有上下限的则可以用atLeast(int min)atLeastOnce()atMost(int max)

4. Use Cases

还是话不多说地从DemoService说起吧,下面我汇集了平时常用的一些情况在一个API中,然后我们尝试着用Mockito来写一下它的测试,并以此为出发点介绍Mockito的一些常见用法。

public class DemoService {
    private DependencyY dependencyY;
    private DependencyYY dependencyYY;
    private DependencyZ dependencyZ;

    public DemoService(DependencyZ dependencyZ) {
        this.dependencyZ = dependencyZ;
    }

    public int run() {
        DependencyX dependencyX = new DependencyX("inputOfX");
        String x = dependencyX.getX();
        List<String> y = dependencyY.getY("default");
        List<String> list = new ArrayList<>();

        for (String s : y) {
            try {
                list.add(dependencyYY.doSomethingForY(s));
            } catch (DemoException ex) {
                // Handle the exception
            }
        }

        int index = doSomething(list, x);
        if (StaticDependency.isEnabled() && -1 == index) {
            dependencyZ.sendNotification("ErrorMessage");
        }

        StaticDependency.doAnything();
        return index;
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({DemoService.class, StaticDependency.class})
public class DemoServiceTest {
    @Mock
    private DependencyX dependencyX;
    @Mock
    private DependencyY dependencyY;
    @Mock
    private DependencyYY dependencyYY;
    @Mock
    private DependencyZ dependencyZ;
    @InjectMocks
    private DemoService demoService = new DemoService(dependencyZ);
    @Captor
    private ArgumentCaptor<String> notificationCaptor;

    @Before
    public void setUp() throws Throwable {
        MockitoAnnotations.initMocks(this);
        PowerMockito.whenNew(DependencyX.class).withAnyArguments().thenReturn(dependencyX);
        PowerMockito.mockStatic(StaticDependency.class);
        PowerMockito.when(StaticDependency.isEnabled()).thenReturn(true);
    }

    @Test
    public void testDemoService_ShouldGetCorrectIndex_WhenXExistsInY() throws Exception {
        when(dependencyX.getX()).thenReturn("str2");
        when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
        when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
        when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
        when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());

        int result = demoService.run();

        Assert.assertEquals(1, result);
        PowerMockito.verifyStatic();
        StaticDependency.doAnything();
        verify(dependencyZ, times(0)).sendNotification(anyString());
    }

    @Test
    public void testDemoService_ShouldSendNotification_WhenXNotExistsInY() throws Exception {
        when(dependencyX.getX()).thenReturn("str3");
        when(dependencyY.getY(eq("default"))).thenReturn(Arrays.asList("B", "A", "C"));
        when(dependencyYY.doSomethingForY("B")).thenReturn("str1");
        when(dependencyYY.doSomethingForY("A")).thenReturn("str2");
        when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());

        int result = demoService.run();

        Assert.assertEquals(-1, result);
        PowerMockito.verifyStatic();
        StaticDependency.doAnything();
        verify(dependencyZ, atLeastOnce()).sendNotification(notificationCaptor.capture());
        Assert.assertTrue(notificationCaptor.getValue().contains("Error"));
    }
}
4.1 mock被测对象中的成员有返回值的方法

如果被测对象中的成员是一个外部依赖,我们需要mock它的那些有返回值的方法,例如dependencyY.getY(x)dependencyYY.doSomethingForY(y)
这是最基本最常见的一种情况,我们直接mock这些dependency类,然后用when/then语句(given/will也类似)给我们用到的那些方法添加behavior。

when(mockedClass.method(params)).thenReturn(mockedResult);
when(dependencyY.getY(anyString())).thenReturn(Arrays.asList("B", "A", "C"));
when(dependencyYY.doSomethingForY("B")).thenReturn("str1");

when(mockedClass.method(params)).thenThrow(exception);
when(dependencyYY.doSomethingForY("C")).thenThrow(new DemoException());
Map<String, String> mapYToSomething = new HashMap<String, String>() {{
    put("B", "str1");
    put("A", "str2");
    put("C", "str3");
}};

when(dependencyYY.doSomethingForY(anyString())).thenAnswer(new Answer<String>() {
    @Override
    public String answer(InvocationOnMock invocation) throws Throwable {
        Object[] args = invocation.getArguments();
        Object mock = invocation.getMock();
        return mapYToSomething.get(args[0]);
    }
});
4.2 mock被测对象中的成员没有返回值的方法

当被测对象依赖外部dependency的方式是一个没有返回值的方法时,我们在测试的时候并不希望这个方法被真正的调用,例如dependencyZ.sendNotification(message),但是我们依旧希望验证的是这个方法在确实被调用了。这个时候需要用verify语句对它们进行验证。

verify(mockedClass [, times(x), timeout(t)]).method(params);
verify(dependencyZ, times(0)).sendNotification(anyString());
verify(dependencyZ, never()).sendNotification(anyString());

verify(dependencyZ).sendNotification(anyString());
verify(dependencyZ, times(1)).sendNotification(anyString());

verify(calcService, timeout(100)).add(20.0,10.0);

和上面when语句里方法的参数一样,这里的参数列表也可以写一个确定的值,或者任意值,或者匹配一定条件的值。

@Captor
private ArgumentCaptor<String> notificationCaptor;

verify(dependencyZ).sendNotification(notificationCaptor.capture());
Assert.assertTrue(notificationCaptor.getValue().contains("Error"));

注意,虽然我把verify放在这个section讲,但是并不表示它只能用来验证返回值为空的方法,有返回值的方法同样适用。只是一般有返回值的方法我们会mock它的返回值,如果它确实拿到那个mocked值,那说明它执行了,就没必要再做多余的验证了。

InOrder inOrder = inOrder(calcService);
inOrder.verify(calcService).add(20.0,10.0);
inOrder.verify(calcService).subtract(20.0,10.0);
4.3 Partial Mocking

虽然Mockito支持用Spy去partial mock一些真实的instance,但是使用它的时候还是需要谨慎。对于spy的mock,最好使用doReturn|Answer|Throw()这类方法。

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");

// You have to use doReturn() for stubbing
doReturn("foo").when(spy).get(0);

还需要注意的是,Mockito的spy只是copy了你所mock的那个真实的instance,你之后的交互应该跟这个copy的spying instance交互,它不能检测到任何跟原来的real instance的交互。例如,上面的List有两个instance,一个是真实创建出来的LinkedList: list,另一个就是带有mock的spy(list)。之后如果我们对list进行操作,spy是不知情的。
此外,对于spy的object不要去试图mock final方法。

Watch out for final methods. Mockito doesn't mock final methods so the bottom line is: when you spy on real objects + you try to stub a final method = trouble. Also you won't be able to verify those method as well.

4.4 mock在被测对象中的直接new出来的对象

当我们需要mock那些直接在被测方法中new出来的对象的时候,直接在单元测试中用Mock还做不到JMockit的Mocked那么强大,比如说上面例子中的dependencyX。我们的做法是用PowerMockito去mock这个类的构造函数,返回一个我们mock的instance。关于PowerMockito的更多信息,我们将在下一节中介绍。
需要注意的是,当使用PowerMockito.whenNew()的的时候需要在PrepareForTest里加上需要new ClassA的被测试的ClassB。例如上面的例子,我们在DemoService类里直接调用new DependencyX()来创建一个DependencyX的实例,我们需要prepare的不是DependencyX而是DemoService。

@Mock
private DependencyX dependencyX;

PowerMockito.whenNew(DependencyX.class).withAnyArguments()
            .thenReturn(dependencyX);
when(dependencyX.getX()).thenReturn("str2");

另外,当需要验证被测试的逻辑中有构造某个对象,则可以用PowerMockito.verifyNew

PowerMockito.verifyNew(MyClass.class).withNoArguments();
4.5 mock static方法

对于静态方法的mock,我们就需要用到PowerMock了。
(0)使用PowerMockito的时候需要在单元测试类上添加annotation @RunWith(PowerMockRunner.class)@PrepareForTest({YourStaticClass.class})
为什么我们需要PrepareForTest呢,这里是官方文档里的介绍。总之,就是告诉PowerMock那些测试中需要mock的类,尤其是那些final类和需要mock私有的,静态的或者native的方法的类。

This annotation tells PowerMock to prepare certain classes for testing. Classes needed to be defined using this annotation are typically those that needs to be byte-code manipulated. This includes final classes, classes with final, private, static or native methods that should be mocked and also classes that should be return a mock object upon instantiation.

当使用这个PrepareForTest的时候,我们还要么加上@RunWith(PowerMockRunner.class),要么用下面的语句来初始化prepare的那些需要mock的东西。

public static TestSuite suite() throws Exception {
    return new PowerMockSuite(MyTestCase.class);
}

(1)Mock一个static类和它的方法,例如上面的StaticDependency. doAnything()

PowerMockito.mockStatic(StaticClass.class);
Mockito.when(StaticClass.staticMethod(param)).thenReturn(value);

(2)验证mock对象的行为。与上面一样,也需要先声明一下verifyStatic,然后直接验证具体的方法,连verify()都不需要了。如果方法有参数列表,则对参数的匹配可以用Mockito.Matchers那一套。注意,这两步是验证一个static方法必不可少的。当需要验证多个static的方法时,每个方法都需要加上verifyStatic()

PowerMockito.verifyStatic(StaticClass.class); 
StaticClass.staticMethod(param);

(3)mock static的方法抛出异常。对于有返回值的方法,也可以直接用when/thenThrow,对于没有返回值的方法,则需要用doThrow/when(staticClass),然后再写相应的方法。

PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.isEnabled());
PowerMockito.doThrow(new RuntimeException()).when(StaticDependency.class);
StaticDependency.doAnything();

PowerMockito.doThrow(new RuntimeException()).when(myFinalMock).myFinalMethod();

(4)mock和verify私有的方法。对于私有的方法,我们直接用mockedClass.是无法访问到的,可以用下面这种方式。

PowerMockito.when(tested, "privateMethodName", argument).thenReturn(value);

PowerMockito.verifyPrivate(tested).invoke("privateMethodName", argument);

(5)跟Mockito类似,我们可以用PowerMockito.spy来实现Partial Mocking,下面是从powermock wiki拷贝的一个完整的例子。

@RunWith(PowerMockRunner.class)
// We prepare PartialMockClass for test because it's final or we need to mock private or static methods
@PrepareForTest(PartialMockClass.class)
public class YourTestCase {
    @Test
    public void spyingWithPowerMock() {        
        PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());

        // use Mockito to set up your expectation
        Mockito.when(classUnderTest.methodToMock()).thenReturn(value);

        // execute your test
        classUnderTest.execute();

        // Use Mockito.verify() to verify result
        Mockito.verify(mockObj, times(2)).methodToMock();
    }
}

References

Mockito Mockito-Core 2.7.12
Tutorialspoint Mockito Overview
Why is Mockito voted better than JMockit
Mockito vs. EasyMock vs. JMockit
Slant: JMockit vs. Mockito
Mockito vs. JMockit
Forming Mockito “grammars”
Parameterized testing with Mockito by using JUnit @Rule
Junit MockitoRule
PowerMock for Mockito
PowerMock Annotation PrepareForTest

上一篇下一篇

猜你喜欢

热点阅读