开发笔记:哦豁,BOSS说单元测试覆盖率不达标?(Java)

目录

前言

关于单元测试

什么是单元测试

为什么要写单元测试

单元测试的三部曲

Mock

什么是mock

为什么使用mock对象

MockMvc

MockMvc相关API

Mockito

Mockito相关工具

PowerMockito

PowerMockito介绍

引入PowerMockito依赖

PowerMockito的使用

PowerMockito API

PowerMockito简单实现原理

开发笔记:如何使用PowerMockito去Mock  TkMybatis的Example

背景说明:

提供方案一:

提供方案二:


前言

不写单元测试的开发不(hui)是(bei)优秀(lao'ban)的开发(zhong'chui)


关于单元测试

    鉴于我们自己对代码负责的角度(肯定不是因为领导的严格要求),我们要对单元测试有一定的了解,并且对每一段新开发的代码都要写单元测试去自测有没有问题。

    这样一来减轻了测试大佬们的压力,二来对代码的质量、健壮性有所保障,三来也是最真实的:测试期少收到几个Bug

什么是单元测试

    单元测试(unit testing)是指对软件中最小的可测试单元进行检查和验证。通常单元测试只测试一个方法或者一个方法调用。一个单元测试的好处:快速定位bug,提高代码质量,通过单测理解代码放心重构。单元测试代码对于代码的重构非常重要,因为一不小心犯了错,这些小范围的测试能很快作出提醒,这样就可以放心的随时调整代码。

    并且在实际的开发中,单元测试会对好代码的编写有一定的帮助,譬如说所有的业务构成一段非常长的方法,在编写该方法的单元测试的时候会感觉到有点难受,有了这样的体验,开发在开发的过程中会斟酌方法是否过长,封装多几个方法会比会比较好。虽然说,可能很多小伙伴写单元测试是上级有明确指标,但是写多了也会发现它的作用,比如说在一些有很多种情况的业务中,有某种情况没考虑清楚,单元测试的时候就可能发现,这很有可能节省我们一次或多次打包发测试环境的工作。

为什么要写单元测试

原因:

  • 遵循测试驱动开发的思想
  • 作为一个优秀开发Boy应该做的
  • 领导下指标了

测试驱动开发(Test Driven Development)是敏捷开发中的一种实践开发模式,其思想就是通过测试不断驱动编写完善的产品代码,实现所需的功能。

它又下面几点原则:

  • 非能让失败的单元测试通过,否则不允许去编写任何的产品代码。
    对于任何功能需求,都是先从写测试用例入手,为满足测试用例才能去写产品代码。

  • 只允许编写刚好能够导致失败的单元测试。 (编译失败也属于一种失败)
    通常在开发完成后写的测试用例都是希望能通过的测试用例,很可能因先入为主导致不能正确覆盖测试。相反,TDD编写新的测试用例是为了覆盖不同的需求,导致失败。

  • 只允许编写刚好能够使一个失败的单元测试通过的产品代码。
    编写的生产代码只能是为了使一个失败的单元测试通过,不应编写多余的实现代码。如果过多编写了实现其他功能业务的代码,则违反了TDD的原则。

它会带来哪些好处呢?

  • 单元测试保证了编码过程中持续重构的正确。在完成代码的编写后,我们会得到很多的测试代码,而这些测试代码可以用来保证对代码后续的维护修改的正确性。单元测试对代码的覆盖率同样可以增强我们对代码的信心。
  • 丰富的单元测试完全可以替代接口文档,更能清楚的描述代码实现的功能。
  • TDD的开发模式促使了代码的抽取和解耦,利于代码复用。这一点也就是我在上面提到的开发过程中会有意识的多做封装

单元测试的三部曲

given-when-then三部曲

Given:初始化或前置条件

When:行为发生

Then:断言结果

可能从理论上看起来不好懂,我们在写单元测试的过程中就是沿着三部曲的思想去做的,这里拿一段代码举个栗子:

// GIVEN
DataSourceRequest dataSource = new DataSourceRequest();
dataSource.setPage(1);
dataSource.setPageSize(10);
when(inquiryMapper.getDemo(any())).thenReturn(newArrayList(new DemandDto()));

这段代码的具体思路就是实例化了一个自己封装的PageHelper的工具类,然后对一些属性进行赋值,这个环节就是三部曲的第一步:初始化或前置条件。然后后面借用Mockito的when方法,也就是我们在要测试的业务类中调用了inquiryMapper.getDemo()方法的地方,any()代表传入任何参数都Mock,这时候我们调用thenReturn返回一个我们初始化好的数据。when和thenReturn分别代表三部曲中的后两部:行为发生和断言结果。

汇总思路:假设我们要去单测xxx类,那么这段代码的意思就是在xxx类中调用inquiryMapper.getDemo()的时候,我们不去真实的调用而是直接返回一个我们预先初始化好的结果newArrayList(new DemandDto())。这就是Mock的体现。


Mock

什么是mock

       在面向对象的程序设计中,模拟(mock)对象就是以可控的方式模拟真实对象的行为的假对象。在编程的过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。mock其实就是在测试过程中,对于一些不容易构造和跑通的对象和方法,通过Mock来模拟其行为。

       这里讲解一下什么叫做测试过程中不容易跑通构造的方法,举一个栗子最多的情况就是Mapper中的方法,本狗子所在的项目组,对于Mapper的方法是直接Mock掉,不去实际调用数据库,其原因有几个,首先用的是使用mockito框架而不是Spring的单测,并且在不同的数据库上的数据的内容有所不同,假设开发库和测试库对不齐,那么开发库打包的时候会因为单测跑不过报错。

       所以对于这种方法,我们不实际调用,当业务代码调用的时候,会直接设置我们初始化好的返回值,也就是Mock这个方法。

为什么使用mock对象

       使用模拟对象,可以模拟复杂的、真实的对象行为。如果单元测试中无法使用真实对象,可采用模拟对象进行代替。

MockMvc

       MockMvc是由spring-test包提供,是服务端Spring MVC测试支持的主入口。可以用来模拟客户端请求,实现了对http请求的模拟,能够直接使用网络形式,转换到controller的调用,不依赖网络环境。同时提供一套验证的工具,使得结果验证比较方便。

使用spring-test编写MockMvc单元测试需要在类上加上:@RunWith(SpringRunner.class)和@SpringBootTest注解,也可以加上@AutoConfigreMockMvc 来注入MockMvc

@RunWith用于指定测试运行器,如果不声明具体的运行器,将使用默认的运行器BlockJUnit4ClassRunner。@WebAppConfiguration注解用于声明测试时所加载的是webApplicationContext,但是它需要结合@ContextConfiguration注解一起使用。表示测试环境使用的ApplicationContext将是WebApplicationContext类型的。

同时也可以用@Before来进行初始化,@Before会执行在@Test之前

示例:

@Before
public void setUp() throws Exception {
    // 实例化方式一
    mockMvc = MockMvcBuilders.standaloneSetup(new XxController()).build();

    // 实例化方式二
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();

    //Mockito实例化方式
    MockitoAnnotations.initMocks(this);
}

MockMvc相关API

    Perform():用于执行请求,通过post或者get传入相关请求

    AndDo():执行普通处理,使用print()方法用于打印请求或者相应及其他信息

    AndExcept():执行预期匹配,如status().ok()表示预期相应成功,或content().string()表示预期的返回结果值

    andReturn():用于返回请求访问对象MvcRsult

    param()和content()用法相识,表示添加请求后传值

示例:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {DemandApplication.class})
@AutoConfigureMockMvc
public class XxControllerTest {
    @Autowired
    IXxService xxService;
    MockMvc mockMvc;
    @Before
    public void setUp() throws Exception {
        // 实例化方式一
        mockMvc = MockMvcBuilders.standaloneSetup(new XxController()).build();
        // 实例化方式二
        //  mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
        //Mockito实例化方式
        //MockitoAnnotations.initMocks(this);
    }
    @Test
    public void closeTest() throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("headerId", "60000");
        String requestJson = JSONObject.toJSONString(jsonObject);
      mockMvc.perform(post("/xx/xx/close").contentType(MediaType.APPLICATION_JSON).content(requestJson))
                .andDo(print())
                .andExpect(status().isOk())
                .andReturn().getResponse().getContentAsString();
    }

    @Test
    public void cloneTest() throws Exception {
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("headerId", "50000");
        String requestJson = JSONObject.toJSONString(jsonObject);
       mockMvc.perform(post("/xx/xx/cancel").contentType(MediaType.APPLICATION_JSON).content(requestJson))
                .andDo(print()).andExpect(status()
                .isOk()).andReturn().getResponse().getContentAsString();
    }
}

 


Mockito

Mockito工具可以和Junit工具组合使用,mockito本质上是一个proxy代理模式的应用。是在代理对象调用方法前,用stub方式设置其返回值,然后在真实调用时,用代理对象返回起预设的返回值。使用Mockito一般分为三个步骤:

  • 1.模拟测试类所需要的外部依赖
  • 2.执行测试代码
  • 3.判断结果是否达到预期

使用mockito测试框架需要使用@RunWith(MockitoJUnitRunner.class)注解来声明测试运行器。然后使用@Before来进行初始化,另外使用mock一个对象,对象为null。使用初始化之后就不会抛出空指针异常。

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

 

Mockito测试框架主要用来测试Service的代码,需要用@Mock来模拟DAO层操作,然后使用@InjectMock来注入我们需要测试的service层。@Mock标注的对象会自动被注入到被@InjectMock标注的对象中。同时对于@mock标注的对象,mockito还提供了参数匹配器argument matchers,例如使用anyString匹配任何String参数,使用anyInt()匹配任何int参数,anySet()匹配任何set集合,对于用户自定义的类型,可以使用argThat()方法来匹配自定义类型。甚至使用any()可以代替任何类型的参数,any()也可以传入某个类来做特殊指定。另外有一点需要注意的是,如果一个方法有多个参数,如果一个参数使用了参数匹配器,其余参数必须都使用参数匹配器。

此外创建mock对象不能对final,Anonymous,static类进行mock。当我们使用mockito时,使用静态导入可以使代码更简洁,如

import static org.mockito.Mockito.*;

Mockito常用方法:

When(Object):为函数进行打桩(即stub,是指完全模拟一个外部依赖),而mock用来判断测试是通过还是失败
Verify():函数可以验证函数的调用次数,注意verify传入的参数必须是一个mock对象
DoThrow():用于对无返回值函数打桩进行异常抛出
Donothing():用于对无返回值函数进行测试
DoReturn():用于自定义函数的返回值
Spy():使用spy()所生成的类,所有方法都是真实方法,fan返回值都是和真实方法是一样的。而使用Mock生成的类,所有方法都不是真实方法,返回值都是null。DocallRealMethod()方法可以获取方法真实的返回值。

Mockito单元测试示例:

@RunWith(JUnit4.class)
public class PageHelpTest {
    @InjectMocks
    private InquiryServiceImpl inquiryService;
    @Mock
    private InquiryMapper inquiryMapper;
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
    /**
     * 分页测试
     */
    @Test
    public void pageTest() {
        // GIVEN
        PageDataSourceRequest dataSource = new PageDataSourceRequest();
        dataSource.setPage(2);
        dataSource.setPageSize(5);
        dataSource.handleUnknown("name", "test");
        when(inquiryMapper.getDemo(any())).thenReturn(new ArrayList(new DemandDto()));
        // WHEN
        PageInfo<DemandDto> page = inquiryService.testPageHelp(new PageParams(dataSource.getPage(),
                dataSource.getPageSize()), dataSource.toObject(DemandDto.class));
        // THEN
        Assert.assertEquals(1, page.getPageSize());
    }
}

 

Mockito相关工具

Mockito API:https://static.javadoc.io/org.mockito/mockito-core/3.0.0/org/mockito/Mockito.html

 

PowerMockito

PowerMockito介绍

在写单元测试的时候,我们会发现我们要测试的方法会引入很多外部依赖对象(比如外部邮件服务、网络通讯和远程微服务调用等),由于我们我所依赖的服务不由我所在的项目组维护(对方接口可能中途会发生变化,甚至,有时候可能并未启动)。集成测试成本略高,故而需要mock工具来模拟这些外部依赖的对象,来完成单元测试。

为什么要引入PowerMockito呢?现如今比较流行的mock工具如Jmock、EasyMock、Mockito等都有一个共同缺陷:不能mock静态、final、private等。而PowerMockito提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMockito现对静态方法、构造方法、私有方法以及final方法的模拟支持,对静态初始化过程的移除等强大的功能。

下面是PowerMockito的官方介绍:

从官方介绍中,我们可以看出PowerMockito使用了Mockito API,我们可以同时使用两个mock框架进行单元测试,PowerMockito 2.0.0以及更高的版本具有Mockito 2的支持,1.7.0及更高的版本具有Mockito 2试用版的支持。PowerMockito旨在用少量的方法和注解扩展现有的API来实现额外的功能。目前PowerMockito支持EasyMock和Mockito。

引入PowerMockito依赖

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.23.0</version>
    <exclusions>
        <exclusion>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>RELEASE</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.2</version>
    <scope>test</scope>
</dependency>

PowerMockito的使用

PowerMockito有两个重要的注解:

  • 1.@Runwith(PowerMockRunner.class)
  • 2.@PrepareForTest( { YourClass.class })

如果你的单元测试李没有使用注解@PrepareForTest,那么可以不用加注解@RunWith(PowerMockRunner.class)。反之,如果当你要使用PowerMockito强大功能(mock静态、final、private等方法时)时,就需要加注解@PrepareForTest,注意花括号{}里面可以创建多个类的实例,用逗号隔开。如

@PrepareForTest({ YourClass.class,MyClass.class })

当我们要mock一个静态方法时,有三种方法去使用,第一种是使用@PrepareForTest注解创建一个静态类实例。

@PrepareTest(Static.class)

第二种使我们需要调用PowerMockito.mockStatic()去mock一个静态类,如

PowerMockito.mockStatic(Static.class);

第三种是使用when并模拟返回值

Mockito.when(Static.firstStaticMethod(param)).thenReturn(value);

当我们需要去验证静态方法的行为时,需要使用PowerMockito.verifyStatic(Static.class)去开启验证,然后直接去调用静态方法即可。如

PowerMockito.verifyStatic(Static.class); 

Static.firstStaticMethod(param); 

PowerMockito参数匹配器的用法和Mockito类似,这里不再赘述,请看看下列例子

PowerMockito.verifyStatic(Static.class);

Static.thirdStaticMethod(Mockito.anyInt());

同时使用verify的使用方法也和Mockito的类似,我们可以使用verifyPrivate去验证行为,verifyPrivate也可以去mock一个private static方法

Mockito.verify(mockObj, times(2)).methodToMock();

verifyPrivate(tested).invoke("privateMethodName", argument1);

使用doThrow去mock一个void方法

PowerMockito.doThrow(new ArrayStoreException("Mock error")).when(StaticService.class);
StaticService.executeMethod();

使用PowerMockito.verifyPrivate去验证new Object的构造器

verifyNew(MyClass.class).withNoArguments();

使用spy去模拟真实调用:

PartialMockClass classUnderTest = PowerMockito.spy(new PartialMockClass());

Mockito.when(classUnderTest.methodToMock()).thenReturn(value);

classUnderTest.execute();

Mockito.verify(mockObj, times(2)).methodToMock();

PowerMockito API

https://static.javadoc.io/org.powermock/powermock-api-mockito/1.6.2/org/powermock/api/mockito/PowerMockito.html

PowerMockito简单实现原理

当某个测试方法被注解@PrepareForTest标注以后,在运行测试用例时,会创建一个新的org.powermock.core.classloader.MockClassLoader实例,然后加载该测试用例使用到的类(系统类除外)。PowerMockito会根据你的mock要求,去修改@PrepareForTest里的class文件(当测试类会自动加入注解中),以满足特殊的mock需求。例如:去除final方法的final标识,在静态方法的最前面加入自己的虚拟实现等。如果需要mock的是系统类的final方法和静态方法,PowerMockito不会直接修改系统类的class文件,而是修改调用系统类的class文件,以满足mock需求。关于更多实现源码请参考:https://github.com/powermock/powermock

 

开发笔记:如何使用PowerMockito去Mock  TkMybatis的Example

背景说明:

我们在写单测的时候,如果去mock example的查询的话会报找不到表的错误,就很迷,于是产生了几种思路:1. 不用example。 2. 单元测试跳过example的地方。这两种思路当然都不可以,如果因为单测而不用example,那么我觉得是给单测束缚住了,这样和初衷不符,如果跳过example的地方的话,那跳过的就太多了,单测覆盖率也低。

 

提供方案一:

@PrepareForTest({XxServiceImpl.class})

说明:XxServiceImpl是自己需要去单测的目标类

缺陷:这种方法虽然可以直接mock  Example的方法,但是如果部门使用sonar来做单元测试覆盖率扫描的话这种方法是扫描不到覆盖率的,对于覆盖率很看重的当然需要摒弃这种做法。

 

提供方案二:

写了较久的单测,觉得对于Example这个坑是肯定要过的,而且肯定要扫描到覆盖率。

所以下面将从找不到表结构开始进行分析如何深入Mock

分析过程

解决找不到表结构问题:

通过ctrl+鼠标左键进去源码查看如何去new一个Example实例的过程:

 

解决方案:

mock调这个方法

//头上记得加@PrepareForTest({EntityTable.class})

//没有表结构就给它表结构

EntityTable table = PowerMockito.mock(EntityTable.class);

PowerMockito.when(EntityHelper.getEntityTable(Mockito.any())).thenReturn(table);

 

criteria.andEqualTo()找不到属性名问题解决:

去看criteria内部的源码报错的地方通过自己Mock代替它的实例化

//头上记得加@PrepareForTest({Example.class})

Example.Criteria criteria = PowerMockito.mock(Example.Criteria.class);

//这里为了将目标业务层中的criteria替换成我们自己mock的实例方便后面mock

PowerMockito.whenNew(Example.Criteria.class).withArguments(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()).thenReturn(criteria);

 

最终解决:(头上记得加@PrepareForTest({Example.class, EntityTable.class}))【EntityTable是Example内部代码调用的类

//mock example
PowerMockito.mockStatic(EntityHelper.class);
//没有表结构就给它表结构
EntityTable table = PowerMockito.mock(EntityTable.class);
PowerMockito.when(EntityHelper.getEntityTable(Mockito.any())).thenReturn(table);

Example.Criteria criteria = PowerMockito.mock(Example.Criteria.class);
//这里为了将目标业务层中的criteria替换成我们自己mock的实例方便后面mock
PowerMockito.whenNew(Example.Criteria.class).withArguments(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()).thenReturn(criteria);

//模拟example查询
PowerMockito.when(criteria.andEqualTo(Mockito.anyString(), Mockito.any())).thenReturn(criteria);

 

转载注明出处。

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_我走路带风

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

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

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

打赏作者

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

抵扣说明:

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

余额充值