单元测试基本套路

好的实现,一定是可测试的。

单元测试(Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。

单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。

单元测试应该单独测试一个类,这就需要排除此类的依赖类造成的影响。

如图所示,要为class A进行单元测试,那么可以通过为其依赖的Class B ,C 的创建模拟对象(mock object)来替代实际对象,然后定义模拟对象调用某个方法后的输出, 来排除依赖的影响。


本文说明了如何基于我们现有的web app项目(java+tomcat+spring),使用Mockito、PowerMock、Spring MVC test等测试框架来编写单元测试。

我们建议对于business、service层的单元测试使用Mockito、PowerMock,而controller的单元测试使用Spring MVC test。

1、使用Mockito

Mockito(http://mockito.org/)是目前较为流行的一个mock框架(Mockito is a mocking framework that tastes really good. It lets you write beautiful tests with a clean & simple API.)

使用Mockito编写单元测试的套路如下:

(1)为测试类的外部依赖定义mock object, 定义调用mock object的行为(此步即为插桩,Stub)

(2)执行测试

(3)验证代码执行是否符合预期

要使用Mockito, 首先引入mockito的依赖

<dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-all</artifactId>

    <version>1.10.19</version>

    <scope>test</scope>

</dependency>

<dependency>

    <groupId>junit</groupId>

    <artifactId>junit</artifactId>

    <version>4.12</version>

    <scope>test</scope>

</dependency>

我们定义如下business

@Business

public class InvokeBusinessImpl implements InvokeBusiness {

    private static final Log LOGGER = LogFactory.getLog(InvokeBusinessImpl.class);

    @Autowired

    private UserService userService;

   

    @Override

    public Map<String, Object> getUsers() {

        String respMsg = "查询失败";

        Map<String, Object> mapRtn = new HashMap<>();

        mapRtn.put("success"true);

        mapRtn.put("respCode", GlobalRespCodes.SUCCESS);

        try {

            List<User> users = userService.getUsers();

            if (users != null && users.size() > 0) {

                mapRtn.put("result", users);

            else {

                mapRtn.put("success"false);

            }

        catch (Exception e) {

            LOGGER.error(respMsg, e);

            mapRtn.put("success"false);

        }

        return mapRtn;

    }

}

可以看到,要为InvokeBusinessImpl这个类编写单元测试,需要为其依赖的UserService创建mock object以排除其影响。

使用Mockito创建mock对象及进行测试步骤如下:

定义一个测试类 InvokerBusinessImplTest。

public class InvokerBusinessImplTest {  

}

(1)使用@Mock注解需要mock的对象

public class InvokerBusinessImplTest {

    @Mock

    private UserService userService ;  

}

(2)使用@InjectMocks注解需要测试的对象,以注入其依赖的对象

public class InvokerBusinessImplTest {

    @Mock

    private UserService userService ; 

 

    @InjectMocks

    private InvokeBusinessImpl invokeBusiness;

}

(3)使用@RunWith(MockitoJUnitRunner.class) 注解测试类,以初始化mock 对象。

@RunWith(MockitoJUnitRunner.class)

public class InvokerBusinessImplTest {

    @Mock

    private UserService userService ; 

 

    @InjectMocks

    private InvokeBusinessImpl invokeBusiness;

}

或者,大家也会看到初始化mock对象的另一种方法,在标注了@Before的方法(通常叫setUp)中调用MockitoAnnotations.initMocks(this)

public class InvokerBusinessImplTest {

    @Mock

    private UserService userService ; 

 

    @InjectMocks

    private InvokeBusinessImpl invokeBusiness;

 

    @Before

    public void setUp(){

        MockitoAnnotations.initMocks(this);

    }  

}

以上两种方式选一种即可,推荐用注解的方式。如果你在单测代码中看到了两者都写了,那就是多余了。

(4)编写测试用例,测试用例用@Test标注,必须是public void的。使用doReturn(……).when(mockObject).someMethod(…)在测试用例中配置mock 对象的行为;

(5)调用测试对象执行测试方法;

(6)使用assert验证返回结果,使用verify(mockObject, times(…)).someMethod(...)验证mock对象的调用次数;

@RunWith(MockitoJUnitRunner.class)

public class InvokerBusinessImplTest {

    @Mock

    private UserService userService ; 

    @InjectMocks

    private InvokeBusinessImpl invokeBusiness; 

 

    @Test

    public void testBusiness(){

        User u = new User();

        u.setName("test");

        List<User> list = new ArrayList<>();

        list.add(u);

        //配置mock 对象的行为

        doReturn(list).when(userService).getUsers();

        //执行测试方法

        Map<String, Object> result = invokeBusiness.getUsers();

        //验证返回结果

        assertEquals(true, result.get("success"));

        verify(userService, times(1)).getUsers();

    }

}

(7) 为每一种能覆盖到的条件编写单元测试

@RunWith(MockitoJUnitRunner.class)

public class InvokerBusinessImplTest {

   ……

    /**

     * 测试失败条件

     */

    @Test

    public void testBusinessFail(){

        doReturn(null).when(userService).getUsers();

        Map<String, Object> result = invokeBusiness.getUsers();

        assertEquals(false, result.get("success"));

 

        List<User> list = new ArrayList<>();

        doReturn(list).when(userService).getUsers();

        result = invokeBusiness.getUsers();

        assertEquals(false, result.get("success"));

 

        verify(userService, times(2)).getUsers();

    }

    /**

     * 测试异常条件

     */

    @Test

    public void testBusinessException(){

        doThrow(new RuntimeException()).when(userService).getUsers();

        Map<String, Object> result = invokeBusiness.getUsers();

        assertEquals(false, result.get("success"));

        verify(userService, times(1)).getUsers();

    }

}

Mockito能覆盖很大一部分单元测试场景,但是它有以下局限,它不能测试以下情况:

(1)静态方法

(2)私有方法

(3)final类

(4)匿名类

Mockito Matchers

Mockito matchers大家会经常使用,Mockito matchers是通过静态方法调用的如下方法:eq, any, gt等。例如常用到为mock的方法传一个任意字符串,那就会用到anyString();

但是,使用matchers不当会碰到InvalidUseOfMatchersException,所以使用matcher的时候请遵守如下规则

(1) 永远只在stubbing的时候,即调用when来mock对象行为或调用verify验证行为的时候使用matcher;

(2) 不要在stubbing的时候混用matcher和具体值,一个stubbing要么都用matcher,要么都用具体值,即避免使用如 when(mockObj.invokemockMethod(anyString(), "another String")) 的写法,

       如果有的方法参数需要具体值,那么使用eq这个matcher包一下,即用 when(mockObj.invokemockMethod(anyString(), eq("another String")))这种写法;

(3) 不要将matcher作为stubbing的返回值, 即避免使用...doReturn(any..eq..)这种写法;

(4) 不要将matcher作为测试方法调用时参数, 即避免使用testObject.invokeTestMethod(any...)这种写法;

 

2、使用PowerMock

PowerMock(https://github.com/jayway/powermock)扩展了Mockito(不仅仅是Mockito,还有EasyMock),可以实现上述Mockito不能满足的测试场景。

我们有如下Service需要测试:

@Service

public class UserService {

    @Autowired

    private DaoTemplate dao;

    public List<User> getUsers() {

 

    public String queryUser(String name) {

        User user = getAUser(name);

        String info = "no such person";

        if (user != null) {

            String hql = "from User where age > ?";

            List<User> users = dao.commonQueryList(hql, user.getAge());

            String houseName = getHouseName(user);

            info = user.toString() + ", from house " + houseName + " age:" + user.getAge() + ",there is " + users

                    .size() + "older one";

        }

        return info;

    }

 

    /**

     * static方法

     *

     * @param user

     * @return

     */

    public static String getHouseName(User user) {

        String name = user.getName();

        String houseName = name.substring(name.lastIndexOf(" ") + 1);

        if (StringUtils.isBlank(houseName)) {

            houseName = "WhiteWalker";

        }

        String land = UserUtil.checkLand(houseName);

        return houseName +" @ "+land;

    }

    private User getAUser(String name) {

        String hql = "from User where name = ?";

        User user = (User) dao.commonQuerySingle(hql, name);

        return user;

    }

}

UserService中包含了一个静态方法getHouseName,一个私有方法getAUser,下面我们使用PowerMock来对这两个方法编写单元测试:
首先添加PowerMock依赖。注意如果引入了PowerMock依赖,那么第1节中的Mockito依赖无需再手动引入,PowerMock本身已依赖Mockito。

<dependency>

    <groupId>org.powermock</groupId>

    <artifactId>powermock-api-mockito</artifactId>

    <version>1.6.3</version>

    <scope>test</scope>

</dependency>

<dependency>

    <groupId>org.powermock</groupId>

    <artifactId>powermock-module-junit4</artifactId>

    <version>1.6.3</version>

    <scope>test</scope>

</dependency>

测试static方法

(1) 编写测试类,在使用PowerMock之前先使用@RunWith(PowerMockRunner.class) 注解测试类

@RunWith(PowerMockRunner.class)

public class UserServiceTest {

}

(2)使用@PrepareForTest注解,@PrepareForTest标明测试中所有需要使用PowerMock进行mock的类。

此处使用@PrepareForTest(UserUtil.class)声明要mock的静态类要测试getHouseName方法,那么我们需要mock掉方法中调用的UserUtil的静态方法checkLand。

@RunWith(PowerMockRunner.class)

@PrepareForTest(UserUtil.class)

public class UserServiceTest {

}

(3) 编写测试方法,调用 mockStatic(UserUtil.class) mock静态类

(4) 为mock的静态类设定静态方法的期望行为

(5) 调用静态方法

(6)验证返回结果

 

@RunWith(PowerMockRunner.class)

@PrepareForTest(UserUtil.class)

public class UserServiceTest {

    @Test

    public void testGetHouseName(){

        mockStatic(UserUtil.class);

        PowerMockito.when(UserUtil.checkLand(anyString())).thenReturn("Seven Kingdom");

        User user = new User();

        user.setName("John Stark");

        String house = UserService.getHouseName(user);

        verifyStatic(times(2));

        assertEquals("Stark @ Seven Kingdom",house);

 

    }

}

测试private方法

为private方法写单元测试与为public方法写单测区别不大,只是在调用要测试方法的时候不同。

对于private方法,可以通过PowerMockWhitebox.invokeMethod方法或者SpringReflectionTestUtils.invokeMethod调用,getAUser的单元测试如下

@RunWith(PowerMockRunner.class)

@PrepareForTest(UserUtil.class)

public class UserServiceTest {

 

    @Mock

    private DaoTemplate dao;

    @InjectMocks

    private UserService userService;

 

    .........

    @Test

    public void testGetUser() {

        User u = new User();

        u.setName("test");

        PowerMockito.doReturn(u).when(dao).commonQuerySingle(anyString(), eq("name"));

        try {

            User result = invokeMethod(userService, "getAUser""name");

            assertEquals("test", result.getName());

            verify(dao, times(1)).commonQuerySingle(anyString(), anyString());

        catch (Exception e) {

            e.printStackTrace();

        }

    }

}

 Mock private方法和static方法

上文中queryUser方法中调用了private方法getAUser,static方法getHouseName,还有依赖对象dao的commonQueryList。

要为queryUser编写单元测试,那么我们要mock以上三个方法的行为。Mockito可以mock掉commonQueryList,但对与getAUser和getHouseName就力不从心了。

PowerMock强大之处就在于能够对private方法、 final 方法、static方法、构造方法的模拟。

static方法的模拟在上文为testGetHouseName写单测的时候已经提到过:我们mock了UserUtil的静态方法checkLand。

为了实现对类的私有方法或者是 Final 方法的模拟操作,需要 PowerMock 提供的另外一项技术:局部模拟。

在之前的介绍的模拟操作中,我们总是去模拟一整个类或者对象,然后使用 When().thenReturn()语句去指定其中值得关心的部分函数的返回值,从而达到搭建各种测试环境的目标。

局部模拟则提供了另外一种方式,在使用局部模拟时,被创建出来的模拟对象依然是原系统对象,虽然可以使用方法 When().thenReturn()来指定某些具体方法的返回值,但是没有被用此函数修改过的函数依然按照系统原始类的方式来执行。

这种局部模拟的方式的强大之处在于,除开一般方法可以使用之外,Final 方法和私有方法一样可以使用。

(1)要使用局部模拟,需要使用的PowerMockito.spy方法来创建局部模拟对象。因为queryUser中也调用了dao的方法,此处同样需要mock,因此我们要为局部对象注入一个dao,mock对象。

为此可以使用@Spy和 @InjectMocks注解创建注入了mock依赖的局部对象,使用此对象来测试queryUser方法

@RunWith(PowerMockRunner.class)

public class UserServiceTest {

    @Mock

    private DaoTemplate dao;

    @Spy

    @InjectMocks

    private UserService userService = new UserService();

}

注意,与上文中测试其他方法时仅使用@InjectMocks不同,使用@Spy注解的对象需要调用构造方法。

(2)为了模拟UserService本身的私有方法和静态方法,需要在测试类上注解@PrepareForTest(UserService.class)表明我们要使用PowerMock来mock UserService的方法

(3)定义模拟对象userService的行为

(4)验证结果

综上queryUser测试如下

 

@RunWith(PowerMockRunner.class)

@PrepareForTest(UserService.class)

public class UserServiceTest {

    @Mock

    private DaoTemplate dao;

    @Spy

    @InjectMocks

    private UserService userService = new UserService();

    @Test

    public void testQueryUser(){

        User u = new User();

        u.setName("Sansa Stark");

        List<User> users= new ArrayList<>();

        u.setAge(10);

        u.setAddress("WinterFall");

        users.add(u);

        //声明局部模拟对象

/*

        UserService service = PowerMockito.spy(new UserService());

*/

        try {

            //模拟私有方法

            PowerMockito.doReturn(u).when(userService, "getAUser", anyString());

            //模拟静态方法

            mockStatic(UserService.class);

            PowerMockito.when(UserService.getHouseName(any(User.class))).thenReturn("Targaryen");

            PowerMockito.doReturn(users).when(dao).commonQueryList(anyString(), anyInt());

            //调用测试方法

            String result = userService.queryUser("John Stark");

            assertEquals("Sansa Stark lives in WinterFall, from house Targaryen age:10,there is 1 older one",result);

        catch (Exception e) {

            e.printStackTrace();

        }

    }

 

 

另外,对于返回值是void的方法,传入的参数可能在调用后被修改掉,而在要测试的方法中又需要使用修改过的参数

 

public void noReturnMethod(Map<String, User> map) {

    if (map == null) {

        map = new HashMap<>();

    }

    map.put("somekey", (User) dao.commonQuerySingle("from User where name = ?""test"));

}

public List<User> invokeVoid() {

    Map<String, User> map = new HashMap<>();

    Map<String, String> map2 = new HashMap<>();

    noReturnMethod(map);

    List<User> list = new ArrayList<>();

    for (Map.Entry<String, User> entry : map.entrySet()) {

        //map必须不为空否则这儿永远测不到

        list.add(entry.getValue());

    }

    return list;

}

如上代码,如果想测试invokeVoid()方法,需要mock掉调用的noReturnMethod方法;然而invokeVoid()还依赖noReturnMethod方法来修改掉map,不然循环里测试覆盖不到;

在这种情况下就需要用到PowerMock的doAnswer方法。

测试写法如下:你需要new一个Answer对象然后重写它的answer(InvocationOnMock invocationOnMock)方法,修改调用方法的传入参数为你的期望值

 

@Test

public void testInvokeoid(){

    UserService service = PowerMockito.spy(new UserService());

    PowerMockito.doAnswer(new Answer<Map<String,User>>() {

        @Override

        public Map<String, User> answer(InvocationOnMock invocationOnMock) throws Throwable {

            Object args[] = invocationOnMock.getArguments();

            User user = new User();

            user.setName("Tester");

            Map<String,User> map = (Map<String,User>)args[0];

            map.put("test",user);

            return map;

        }

    }).when(service).noReturnMethod(any(Map.class));

    List<User> users = service.invokeVoid();

    assertEquals(1,users.size());

    User u = users.get(0);

    assertEquals("Tester",u.getName());

}

 

 

3. Spring MVC Test

诚然,controller的单元测试可以如同上面测试buiness和service那样只针对一个类的方式来写。

但是对于controller来说,这就无法测试到像请求映射、数据绑定、验证等这些spring本身去处理的事情。

Spring MVC Test提供了一种使用http请求的方式来测试controller的方法,并通过真正的Dispatcher来应答请求,从而也能测试到Spring基础设施是否正确。

这就如同你手动调用了一下controller的接口来进行测试,但是这种方式测试controller的时候同样也需要mock掉它的依赖对象,所以这更像是一种介于单元测试和集成测试之间的方式。

 官方描述如下:

It’s easy to write a plain unit test for a Spring MVC controller using JUnit or TestNG: simply
instantiate the controller, inject it with mocked or stubbed dependencies, and call its methods passing
MockHttpServletRequest, MockHttpServletResponse, etc., as necessary. However, when
writing such a unit test, much remains untested: for example, request mappings, data binding, type
conversion, validation, and much more. Furthermore, other controller methods such as @InitBinder,
@ModelAttribute, and @ExceptionHandler may also be invoked as part of the request processing
lifecycle.

The goal of Spring MVC Test is to provide an effective way for testing controllers by performing requests
and generating responses through the actual DispatcherServlet.

(1)要使用Spring mvc test框架,需要引入spring-test依赖,spring版本要高于3.2

<dependency>

    <groupId>org.springframework</groupId>

    <artifactId>spring-test</artifactId>

    <version>${spring.version}</version>

    <scope>test</scope>

</dependency>

(2)使用@RunWith(SpringJUnit4ClassRunner.class)注解类

(3)使用@WebAppConfiguration注解类,声明应用上下文(ApplicationContext)为WebApplicationContext;

 (4)使用@ContextConfiguration("classpath:applicationContext-test.xml")注解类来定义构造上下文使用的配置。

applicationContext-test.xml应位于test目录的class目录下,内容与应用实际上下文配置基本相同。

demo应用的applicationContext-test.xml配置如下:

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xmlns:p="http://www.springframework.org/schema/p"

       xmlns:context="http://www.springframework.org/schema/context"

       xmlns:tx="http://www.springframework.org/schema/tx"

       xmlns:cmbus="http://ccd.cmbchina.com/pluto/cmbus"

       xmlns:fbrf="http://ccd.cmbchina.com/pluto/cmbus/properties/filebackfactory"

       xmlns:mvc="http://ccd.cmbchina.com/schema/mvc"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd

       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd

       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd

       http://ccd.cmbchina.com/pluto/cmbus http://ccd.cmbchina.com/pluto/cmbus/schema.xsd

       http://ccd.cmbchina.com/pluto/cmbus/properties/filebackfactory http://ccd.cmbchina.com/pluto/cmbus/properties/filebackfactory.xsd

       http://ccd.cmbchina.com/schema/mvc  http://ccd.cmbchina.com/schema/mvc/spring-mvc.xsd">

 

    <context:component-scan base-package="com.cmbchina.ccd.pluto.demo" use-default-filters="false">

        <context:include-filter type="annotation" expression="org.springframework.stereotype.Controller" />

    </context:component-scan>

 

    <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"

          p:location="classpath:spring-config.properties"/>

 

    <bean id="invokeBusiness" class="org.mockito.Mockito" factory-method="mock">

        <constructor-arg value="com.cmbchina.ccd.pluto.demo.business.InvokeBusiness"/>

    </bean>

 

    <mvc:annotation-driven>

        <mvc:message-converters>

            <bean class="org.springframework.http.converter.StringHttpMessageConverter">

                <constructor-arg name="defaultCharset" value="UTF-8"/>

            </bean>

        </mvc:message-converters>

    </mvc:annotation-driven>

    

</beans>

 

因为我们关注的是web层的测试,所以component-scan我们仅将Controller扫描成bean, 并且声明controller依赖business的mock bean。即在applicationContext-test.xml声明如下:

 

<bean id="invokeBusiness" class="org.mockito.Mockito" factory-method="mock">

    <constructor-arg value="com.cmbchina.ccd.pluto.demo.business.InvokeBusiness"/>

</bean>

 

(4)注入WebApplicationContext成员变量 、声明MockMvc成员变量

(5)在测试前,调用MockMvcBuilders.webAppContextSetup(this.wac).build(),通过加载spring上下文配置、注入一个WebApplicationContext创建MockMvc实例。

 到目前为止,测试前的准备完成如下。

@RunWith(SpringJUnit4ClassRunner.class)

@WebAppConfiguration

@ContextConfiguration("classpath:applicationContext-test.xml")

public class InvokeMvcTest {

    @Autowired

    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before

    public void setUp() {

        mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();

    }

}

 (6)controller如下

@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}, value = "/invokeTest")

@ResponseBody

public String invokeGreetingService(String name, String address) {

    Map<String,Object> resultMap = new HashMap<>();

    resultMap.put("success",false);

    if(StringUtils.isBlank(name)){

        resultMap.put("respMsg","name不能为空");

        return JsonUtils.objectToJsonString(resultMap);

    }

    if(StringUtils.isBlank(address)){

        resultMap.put("respMsg","address不能为空");

        return JsonUtils.objectToJsonString(resultMap);

    }

    return JsonUtils.objectToJsonString(invokeBusiness.greetingService(name, address));

}

 

则其单测写法如下

@Test

public void testInvokeGreetingService() throws Exception {

    Map<String, Object> resultMap = new HashMap<>();

    resultMap.put("success"true);

    Mockito.doReturn(resultMap).when(invokeBusiness).greetingService(anyString(), anyString());

 

    String result = mockMvc.perform(post("/invokeTest").accept(MediaType.APPLICATION_JSON)).andReturn()

            .getResponse().getContentAsString();

    Map<String, Object> respMap = JsonUtils.stringToMap(result);

    assertFalse((Boolean) respMap.get("success"));

 

 

    result = mockMvc.perform(post("/invokeTest").param("name""userName").accept(MediaType.APPLICATION_JSON))

            .andReturn().getResponse().getContentAsString();

    respMap = JsonUtils.stringToMap(result);

    assertFalse((Boolean) respMap.get("success"));

 

    result = mockMvc.perform(post("/invokeTest").param("name""userName").param("address""address").accept

            (MediaType.APPLICATION_JSON)).andReturn().getResponse().getContentAsString();

    respMap = JsonUtils.stringToMap(result);

    assertTrue((Boolean) respMap.get("success"));

 

}

通过mockMvc的perform方法来进行Restful调用,发送一次http请求,accept方法添加请求的ContentType, param方法添加请求参数

然后使用andReturn().getResponse().getContentAsString();获取返回结果的内容串,然后对结果进行验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

退役人员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值