3

最开始项目中是没有单元测试的,基本都是自己通过各种方式来实现测试的。比如修改代码,测完再改回来;再比如直接模拟用户操作,直接当黑盒测试,然后自己去看相应的逻辑有没有,状态有没有改变。

这些方式有几个缺点:

  • 测试不完整,挖有一些隐藏的坑

  • 改代码测试,在该回来的时候可能引入新bug

  • 手工测试比较耗时

  • 下次改需求时,需要再次手工测试

这个里面多次手工测试比较难受,太浪费时间了。以前由于一个逻辑牵扯比较多,构造对象比较复杂,仅仅用JUnit写测试的工作量还是太大,所以单元测试一直没有进行下去。
后来引入的mockito框架来用于新代码的测试,powermock用于以前的代码测试。下面将介绍一下mockito和powermock框架,就明白为什么要用这两个框架了。


Mockito

mockito是用的比较广的mock框架。mock技术的目的和作用是模拟一些在应用中不容易构造或者比较复杂的对象,从而把测试与测试边界以外的对象隔离开。
为了说明使用方法,先引入一下基本对象

public class User {
    private int userId;
    private ComplexObject complexObject;
    
    public int getUserId() {
        return userId;
    }
    //... construction getter
}
public class Service {
    public boolean checkUser(User user) {
        if(user.getUserId() < 100){
            return true;
        }
        return false;
    }
}

默认的static import

import static org.mockito.Mockito.*;
import static org.junit.Assert.*;

Mock

如果要测试Service#checkUser方法,我们就要构造User对象。假设ComplexObject构造很复杂,如果不用mock,测试将寸步难行。下面来看看mockito是如何构造一个假的User并进行测试的吧。

@Test
public void testCheckUser() throws Exception {
    Service service = new Service();
    User user = mock(User.class);
    when(user.getUserId()).thenReturn(2);
    boolean checkResult = service.checkUser(user);
    assertTrue(checkResult);
}

上面可以看到只用mock方法就可以了,然后设置一下getUserId方法的返回就行了。when的语法理解很容易,就不解释了。
上面的when语句也可以换成

doReturn(2).when(user).getUserId();

在这个例子中,这两种when的写法都是可行的。
一共有以下几种方式来模拟一个方法。

  • doReturn

  • doCallRealMethod

  • doNothing

  • doThrow

  • doAnswer

当然也有thenXXX这种形式。

Spy

spy和mock很像,都是模拟一个对象。但是mock是把所有方法都接管了,spy是默认调用对象的方法。如果先mock出一个对象,然后对每一个方法调用doCallRealMethod,这就相当于spy出一个对象。
所以spy和mock只是初始模拟对象的默认设置不一样而已,其他行为都是一样的。

Annotation

可以直接用注解来实现mock:

@Mock
User user;

@Before
public void initMocks() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void testCheckUser() throws Exception {
    Service service = new Service();
    doReturn(2).when(user).getUserId();
    boolean checkResult = service.checkUser(user);
    assertTrue(checkResult);
}

这个需要调用initMocks(this)来注入,这里是通过@Before,也可以通过@RunWith来调用initMocks方法。

也可以用Spy注解:

@Spy User user;

还有一个注解比较有用@InjectMocks,这个可以把对象注入到其他对象中去的。
下面稍微添加一下代码:

public class ComplexObject {
    @Override
    public String toString() {
        return "Complex lhcpig";
    }
    //...
}
public class Service {
    public String handleUser(User user){
        return user.getComplexObject() + "";
    }
    //...
}
public class TestService {

    @InjectMocks
    User user;
    @Spy
    ComplexObject complexObject;

    @Before
    public void initMocks() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testHandleUser() throws Exception {
        Service service = new Service();
        String s = service.handleUser(user);
        assertThat(s, is("Complex lhcpig"));
    }
    //...
}
  • 注1:这里和之前的那个test有冲突,因为User的注解不一样,所以第一个test会报NotAMockException或者MissingMethodInvocationException异常。

  • 注2:这里用Spy,可以不用额外代码,就CallRealMethod。

Verify

这个是用来判断方法是否被调用,调用是否超时,调用了多少次等测试。

@Test
public void testCheckUser() throws Exception {
    Service service = new Service();
    when(user.getUserId()).thenReturn(2);
    boolean checkResult = service.checkUser(user);
    assertTrue(checkResult);
    verify(user).getUserId();
    verify(user, timeout(100)).getUserId();
    user.getUserId();
    verify(user, times(2)).getUserId();
}

如果方法有参数,也可以验证参数。
这里只是简介,如果想详细了解Mockito,建议还是看官网文档


PowerMock

Mockito不支持final方法,私有方法,静态方法,而PowerMock支持。所以这里也要介绍一下。但是还是不建议项目中使用,如果需要使用PowerMock才能测试,说明代码的可测试性不好,需要改进代码。一般都是历史遗留代码或者第三方库相关测试的时候才需要使用。

下面是使用方式

@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
public class YourTestCase {
...
}

给个例子,大家就理解了

@RunWith(PowerMockRunner.class)
@PrepareForTest( { Service.class })
public class TestService {

    @Before
    public void initMocks() {
        mockStatic(Service.class);
    }

    @Test
    public void testTestStaticFinal() throws Exception {
        PowerMockito.when(Service.testStaticFinal()).thenReturn("mock1");
        assertEquals("mock1", Service.testStaticFinal());
    }

    @Test
    public void testPrivate() throws Exception {
        Service t = mock(Service.class);
        PowerMockito.when(t, "testPrivate").thenReturn("xxx");
        doCallRealMethod().when(t).testPrivateForPublic();
        assertEquals("xxx", t.testPrivateForPublic());
    }

    @Test
    public void testTestPrivateWithArg() throws Exception {
        Service t = spy(new Service());
        String arg = "dd";
        PowerMockito.when(t, "testPrivateWithArg", arg).thenReturn("lhc");
        assertEquals("lhc", t.getTestPrivateWithArg(arg));
    }
}
public class Service {

    public static final String testStaticFinal() {
        System.out.println("testStaticFinal");
        return "static final";
    }

    private String testPrivate() {
        System.out.println("testPrivate");
        return "private";
    }

    public String testPrivateForPublic() {
        System.out.println("testPrivateForPublic");
        return testPrivate();
    }

    private String testPrivateWithArg(String arg) {
        System.out.println("testPrivateWithArg");
        return arg + "x";
    }
}

私有方法用PowerMock测试后,如果要修改名字就会很麻烦,重构起来也可能会影响测试用例。所PowerMock的正确使用方式是尽量不使用。

因为要反射调用私有方法,所以写法没有mockito那么优雅。我这里使用的是基于Mockito的PowerMock,所以可以混合使用,比如上面用到的spy,when等。当然PowerMock还有基于其他mock框架(EasyMock)的扩展,这里就不再进一步介绍了。

想让测试更加高效,测试框架还是其次,写出可测试性的代码才是最重要的。


lhcpig
547 声望9 粉丝

Java工程师