为了账号安全,请及时绑定邮箱和手机立即绑定

公司要求单测覆盖率90%?

当业务或者产品快速发展的时候,技术追求的是效率,快速迭代,先把功能堆上去就完事;
当业务已经稳定,产品功能基本完善的情况下,技术上要求质量,质量有多个方面来把控;对技术来说有个最基本和最重要的事情,一般有单元测试的要求。

比如说公司要求单元测试覆盖率要达到90%? 太难完成了…
现在开始要求单元测试率达到85%?

为什么需要单元测试?

  • 业务稳定的时候,或者业务对安全生产的要求大于效率的时候,组织一定会要求写单元测试,要求单元测试的覆盖率。
  • 站在个人层面,通过单元测试,可以减少代码的Bug,让个人的代码质量更高;
  • 在团队协作的时候,有时候不需要等联调数据准备完毕,就可以通过Mock数据的方式让过程进行下去

这其实是一件还挺费力的事情,在这里梳理下单测常用的工具,也许在互联网发展趋于稳定的情况下,可能越来越多的业务都开始有这种要求吧,不过站在个人层面,做技术的同学代码质量高些对自己总是有好处的。

环境

引入常用的单测pom, 中间可能有一些兼容性问题:
PowerMock&Mock兼容性关系:https://github.com/powermock/powermock/wiki/Mockito#maven-configuration

Junit 4.X

最流行的单元测试框架吧,相比testng感觉更成熟,和其他框架的集成更简单一点,所以还是使用了junit4

    <dependency>
				<groupId>junit</groupId>
				<artifactId>junit</artifactId>
				<version>4.12</version>
			</dependency>

Mockito 2.25.0

用来Mokc单测函数运行过程中需要的各种数据结果;

         <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.12.4</version>
            <scope>test</scope>
        </dependency>

Mockito inline 3.12.x

一些方法可能依赖了静态的方法,需要这个依赖

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>3.12.4</version>
            <scope>test</scope>
        </dependency>

PowerMock - 可选

一些私有方法的Mock,一般情况下不建议Mock私有方法,否则最好重构下方法。

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4-rule-agent</artifactId>
            <scope>test</scope>
            <version>2.0.9</version>            
        </dependency>

        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4</artifactId>
            <scope>test</scope>
            <version>2.0.9</version>
        </dependency>
        <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-api-mockito2</artifactId>
            <scope>test</scope>
            <version>2.0.9</version>
        </dependency>

支持Mockito-inline

在src/test/resources/mockito-extensions 目录下新建一个文件org.mockito.plugins.MockMaker,然后里面放这一行内容:mock-maker-inline。

完整依赖

引入以下maven,然后再增加下Mockito-inline

  <dependency>
            <groupId>org.powermock</groupId>
            <artifactId>powermock-module-junit4-rule-agent</artifactId>
            <scope>test</scope>
            <version>2.0.9</version>
        </dependency>

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



        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-inline</artifactId>
            <version>3.12.4</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>3.12.4</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

常用方式Demo

这个Demo主要包含的内容,个人常用的主要就是以下几种:

  • 简单单元测试
  • Mock一些服务返回数据
  • Mock静态方法数据
  • Mock Final的方法数据
  • 不能Mock的直接set私有方法值

要测试的类

主测试类


/**
 * @author : aihe aihe.ah@alibaba-inc.com
 * @date : 2022/9/9 8:06 AM
 * 使用场景:
 * 功能描述:
 */

public class TestServiceImpl {

    private AddDelegate addDelegate;

    private FinalClass finalClass;

    private int factor;

    public int add(int a, int b) {
        return a + b;
    }

    public int addUseDelegate(int a, int b) {
        return addDelegate.add(a,b);
    }


    public int addUseBean(int a,int b){
        AddDelegate bean = SpringContextUtil.getBean(AddDelegate.class);
        return bean.add(a,b);
    }


    public int addWithPrivate(int a,int b){
        return complextAdd(a,b);
    }

    private int complextAdd(int a,int b){
        return (a + b) * factor;
    }


    public int addWithFinal(int a,int b){
        int i = finalClass.finalMethd(a, b);
        return i;
    }

}

依赖的静态类

import java.util.Map;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * SpringContextUtil
 *
 */
@Component("springContextUtil")
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext ctx;


    @Override
    public void setApplicationContext(ApplicationContext arg0) throws BeansException {
        ctx = arg0;
    }

    /**
     * 根据bean的名称,获取bean对象
     * @param beanName bean名称
     * @return 返回获取到的对象
     */
    public static Object getBean(String beanName) {
        return ctx.getBean(beanName);
    }

    /**
     * 根据bean的名称和class类型,获取bean对象
     * @param beanName bean名称
     * @param clazz 对象的class
     * @return 返回获取到的对象
     */
    public static <T> T getBean(String beanName, Class<T> clazz) {
        return ctx.getBean(beanName, clazz);
    }

    /**
     * 根据class类型,获取bean对象
     * @param clazz 对象的class
     * @return 返回获取到的对象
     */
    public static <T> T getBean(Class<T> clazz) {
        return ctx.getBean(clazz);
    }

    /**
     * 查询spring工厂中是否包含该名称的bean对象
     * @param beanName bean名称
     * @return spring工厂中是否包含该名称的bean对象
     */
    public static boolean containsBean(String beanName) {
        return ctx.containsBean(beanName);
    }

    /**
     * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。<br />
     * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
     * @param beanName bean名称
     * @return 返回该对象是否是singleton
     * @throws NoSuchBeanDefinitionException
     */
    public static boolean isSingleton(String beanName) throws NoSuchBeanDefinitionException {
        return ctx.isSingleton(beanName);
    }

    /**
     * 如果给定的bean名字在bean定义中有别名,则返回这些别名
     * @param beanName bean名称
     * @return 返回该对象的别名
     * @throws NoSuchBeanDefinitionException
     */
    public static String[] getAliases(String beanName) throws NoSuchBeanDefinitionException {
        return ctx.getAliases(beanName);
    }

    /**
     * 获取指定类型的所有bean
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> type) {
        return ctx.getBeansOfType(type);
    }

    /**
     * 自动注入目标对象依赖的bean对象。
     *
     * @param t
     * @param <T>
     * @return
     */
    public static <T> T autowireBean(T t) {
        ctx.getAutowireCapableBeanFactory().autowireBean(t);
        return t;
    }
}

依赖的final类

public final class FinalClass {

    public final int finalMethd(int a,int b){
        return a + b;
    }
}

单元测试类


import junit.framework.TestCase;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import static org.junit.Assert.assertNotEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;

/**
 * @author : aihe 
 * @date : 2022/9/9 8:07 AM
 * 使用场景:
 * 功能描述:
 */
@PrepareForTest({SpringContextUtil.class})
@RunWith(PowerMockRunner.class)
public class TestServiceImplTest extends TestCase {

    @InjectMocks
    private TestServiceImpl testService;

    @Mock
    AddDelegate addDelegate;

    @Override
    protected void setUp() throws Exception {
        MockitoAnnotations.openMocks(this);
    }

    @BeforeClass
    public static void init() {
        try {
            springContextUtilMockedStatic = Mockito.mockStatic(SpringContextUtil.class);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            e.printStackTrace();
        }
    }

    @AfterClass
    public static void close() {
        try {
            springContextUtilMockedStatic.close();
        } catch (Exception e) {
            //
        }
    }

    /**
     * 最简单的Mock,没有任何的依赖
     */
    public void testAdd() {
        int res = testService.add(1, 2);
        assertEquals(3, res);
    }

    /**
     * Mock其他的对象返回结果,一般复杂一点的项目只是引入的delegate更多了,需要Mock的数据更多了,但是整体方式如上
     */
    public void testAddDelegate() {
        Mockito.when(addDelegate.add(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())).thenReturn(4);
        int res = testService.addUseDelegate(2, 2);

        assertEquals(4, res);

        res = testService.addUseDelegate(1, 2);

        assertNotEquals(3, res);
    }

    private static MockedStatic<SpringContextUtil> springContextUtilMockedStatic;

    /**
     * Mock静态方法;
     * 参考文档:https://www.baeldung.com/mockito-mock-static-methods
     * 1、要引入Mockito - inline
     * 2、在src/test/resources/mockito-extensions 目录下新建一个文件org.mockito.plugins.MockMaker,然后里面放这一行内容:mock-maker-inline
     * 3、在测试类的文件上加上要Mock的静态类:
     *
     * @PrepareForTest({SpringContextUtil.class})
     * @RunWith(PowerMockRunner.class) 4、要新定义一个属性指定要Mock的对象; private static MockedStatic<SpringContextUtil>
     * springContextUtilMockedStatic;
     * 5、Mockito.mockStatic(SpringContextUtil.class);只能使用一次,因此在beforeClass的时候用,然后再类关闭的时候停掉。
     */
    public void testAddUseBean() {
        Mockito.when(addDelegate.add(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())).thenReturn(4);
        when(SpringContextUtil.getBean(AddDelegate.class)).thenReturn(addDelegate);
        int res = testService.addUseDelegate(2, 2);

        assertEquals(4, res);

        res = testService.addUseDelegate(1, 2);

        assertNotEquals(3, res);
    }

    /**
     * 一些静态属性当无法Mock的时候,直接set进目标值
     */

    public void testPrivate() throws IllegalAccessException {
        FieldUtils.writeField(testService, "factor", 1, true);
        int i = testService.addWithPrivate(1, 2);
        assertEquals(3, i);
    }

    @Mock
    FinalClass finalClass;

    /**
     * Mock一些Final的方法和类,同静态方法
     * 1、Mockito - inline
     * 2、配置支持Mockito - inline
     * 3、可以直接使用Mockito来Mock final的方法了
     *
     * @throws IllegalAccessException
     */
    public void testFinal() throws IllegalAccessException {
        when(finalClass.finalMethd(anyInt(), anyInt())).thenReturn(4);
        int res = testService.addWithFinal(2, 2);

        assertEquals(4, res);

        res = testService.addWithFinal(1, 2);

        assertNotEquals(3, res);
    }
}

复杂的断言

有时候断言判断比较复杂,判断返回的集合内容,更复杂的字符判断,包含等。

  • 判断返回的字符中是否包含一些关键字
  • 判断返回的集合中是否包含某个元素
  • 判断返回的对象中是否有某个属性

如果有更复杂的断言诉求,可以再引入Hamcrest,可以帮忙写更复杂的判断规则。

引入hamcrest包 2.2

<dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest</artifactId>
            <version>2.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

支持的一些匹配规则:
主要Mactcher列表:在下面的代码中对相应的匹配器有做解释

  • 核心:anything,describedAs,is
  • 逻辑:allOf,anyOf,not
  • 对象:equalTo,hasToString,instanceOf,isCompatibleType,notNullValue,nullValue,sameInstance
  • Beans:hasProperty
  • 集合:array,hasEntry,hasKey,hasValue,hasItem,hasItems,hasItemInArray
  • 数字:closeTo,greaterThan,greaterThanOrEqualTo,lessThan,lessThanOrEqualTo
  • 字符:equalToIgnoringCase,equalToIgnoringWhiteSpace,containsString,endsWith,startsWith

hamcrest使用

import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Test;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class HelloAppTest {

    /**
     * 测试中最常用的equalTo。not内部增加了对equalTo的取反,本质上用的还是equalTo。
     * equalTo使用对象的equals方法来进行比较
     */
    @Test
    public void testTutorial(){
        TestObject first = new TestObject(0L,"123");
        TestObject second = new TestObject(1L,"123");
        TestObject third = new TestObject(0L,"123");
        assertThat(first, not(second));
        assertThat(first, equalTo(third));
        assertThat(second,not(third));

    }

    /**
     * Java中的任何对象都可以,不管任何的比较都会返回true
     */
    @Test
    public void testAnything(){
        assertThat("str",anything());
        assertThat(0,anything());
        assertThat(true,anything());
        assertThat(null,anything());
    }

    /**
     * 有时候我们觉得官方的测试失败消息不是那么的已读,那么自己重新指定错误提示消息。
     */
    @Test
    public void testdescribedAs(){
        String str = "str";
        String otherStr = "otherStr";
        assertThat(str,describedAs("当前的字符串是 %0",anything(),str));
        assertThat(str,describedAs("判断字符串 %0 是否等于 %1",is(otherStr),str,otherStr));
    }


    /**
     * allOf 当内部所有的Matcher都成立时才成立,相当于 &&
     * anyOf 当内部任何一个的Matcher成立时就成立,相当于 ||
     * not 对以上取反
     */
    @Test
    public void testLogical(){
        String str = "testStr testStr";
        assertThat(str,allOf( containsString("test"),containsString("Str"),containsString(" ")  ));
        assertThat(str,anyOf( containsString("test"),containsString("adas"),containsString(" ")  ));
        assertThat(str,allOf( containsString("test"),containsString("adas"),containsString(" ")  ));

    }

    /**
     * 测试Object的一些特性:
     *
     */
    @Test
    public void testObject(){
        TestObject testObject = new TestObject(10L,"12313");
        assertThat(testObject, instanceOf(TestObject.class) );
        assertThat(testObject, instanceOf(Object.class) );
        assertThat(testObject, sameInstance(Object.class) );
    }

    /**
     * 测试JavaBean是否有某个属性
     */
    @Test
    public void testBean(){
        TestObject testObject = new TestObject(10L,"12313");
        assertThat(testObject,hasProperty("id"));
    }


    /**
     * 测试集合
     */
    @Test
    public void testCollections(){
        HashMap<Object, Object> map = new HashMap<>();
        map.put("orgId",123);
        map.put("operator",0);
        assertThat(map,hasKey("orgId"));
        assertThat(map,hasValue(123));

        List<Object> list = Arrays.asList("apple","banana","strawberry");
        assertThat(list,hasItem("apple"));
        assertThat(list.toArray(),hasItemInArray("apple"));
    }

    /**
     * 测试数字的比较
     */
    @Test
    public void testNumber(){
        int score = 90;
        int total = 100;
        assertThat(score,lessThan(total));
//        assertThat(score,greaterThan(total));
//        assertThat(score,greaterThanOrEqualTo(total));
//        assertThat(score,lessThanOrEqualTo(total));
    }

    @Test
    public void testText(){
        String text = "Hamcrest comes bundled with lots of useful matchers, but you’ll probably find that you need to create your own from time to time to fit your testing needs. This commonly occurs when you find a fragment of code that tests the same set of properties over and over again (and in different tests), and you want to bundle the fragment into a single assertion. By writing your own matcher you’ll eliminate code duplication and make your tests more readable!";
        assertThat(text,containsString("comes"));
        assertThat(text,startsWith("Hamcrest"));
        assertThat(text,endsWith("readable!"));
        assertThat(text,equalTo("readable!"));

    }

    /**
     * 测试自定义的匹配器
     */
    @Test
    public void testCustomMatcher(){
        String o = "";
        assertThat(o,new isSuccess());
        assertThat(null,new isSuccess());
    }


    /**
     * 只要对象不为空就为成功
     */
    public static class isSuccess extends TypeSafeMatcher{

        @Override
        protected boolean matchesSafely(Object o) {
            return o != null;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("is success");
        }
    }


    public static class TestObject{
        Long id;
        String msg;

        public TestObject(Long id, String msg) {
            this.id = id;
            this.msg = msg;
        }

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TestObject that = (TestObject) o;
            return id.equals(that.id) &&
                    msg.equals(that.msg);
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, msg);
        }
    }
}

自定义Macther

1 继承Matcher类,然后写matcher逻辑,加上macther的描述信息。
2 使用自定义Matcher

   /**
     * 只要对象不为空就为成功
     */
    public static class isSuccess extends TypeSafeMatcher{

        @Override
        protected boolean matchesSafely(Object o) {
            return o != null;
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("is success");
        }
    }
    
   @Test
    public void testCustomMatcher() {
        String o = "";
        assertThat(o, new isSuccess());
        assertThat(null, new isSuccess());
    }    

单元测试覆盖率

写了一堆单元测试,怎么知道自己的单测覆盖度有多少呢?发布的时候是需要检验单测的覆盖度的;

使用Idea默认的Coverage功能,运行后会展示单元测试的覆盖率:

一些注意事项

测试要点

  1. 验证返回值,保证返回值的正确性。 Assert与Hamcrest
  2. 验证方法调用,验证方法被调用到了。 对应Mockito.verify()
  3. 验证方法参数;验证参数传递的正确性。 对应Mockito.verify()
  4. 验证异常信息,保证抛出异常的正确性。 Mockito.doThrow 或 Mockito.when().thenThrow

一些问题

  • 未 Mock 方法或 Mock 方法参数不匹配时,会返回默认值(基础类型为 0,对象类型为 null); 可以使用Mockito. doCallRealMethod来调用真正的方法。
  • Mockito 的 any 相关的参数匹配方法并不支持可空参数和空参数,应该使用 nullable 方法;
  • Mockito再Mock primary类型时,提供了对应的参数,应优先使用。比如当参数为int类型时,使用anyInt而不是any,同理使用anyLong而不是any。

最后

这里主要记录一些在写单元测试过程中用到的东西,偏工具和实用层面;期望能对大家有所帮助。

参考

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
JAVA开发工程师
手记
粉丝
1.1万
获赞与收藏
1544

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消