单元测试实战(四种覆盖详解、测试实例)

理论部分

前言

单元测试,就是对某一段细粒度的Java代码的逻辑测试。代码块一般指一个Java 方法本身,所有外部依赖都需要mock掉,仅关注代码逻辑本身。

需要注意,单测的一个大前提就是需要清楚的知道自己要测试的程序块所预期的输入输出,然后根据这个预期和程序逻辑来书写case。

(这里需要注意的就是单测的预期结果 一定要针对需求/设计逻辑去写,而不是针对实现去写,否则单测将毫无意义,照着错误的实现设计出的case也很可能是错的)

覆盖类型

1、行覆盖 Statement Coverage

行覆盖(又叫语句覆盖)就是通过设计一定量的测试用例,保证被测试的方法每一行代码都会被执行一遍。

路径覆盖是最弱的覆盖方式。

实例:

public Integer fun3(Integer a, Integer b, Integer x) {

        if (a > 1 && b == 0) {
            x = x + a;
        }
        if (a == 2 || x > 1) {
            x += 1;
        }
        return x;
    }

本例仅需要一个case,即可实现行覆盖。test case 如下:


a


b


x


预期结果


TC1


2


0


3


6

@Test
    public void testFun3StatementCoverage(){
        Integer res = demoService.fun3(2,0,3);
        Assert.assertEquals(6,res.intValue());
    }
 

这个用例就可以保证所有的行都被执行。

但是仅仅有这一个用例的话,对这个方法的测试就是非常脆弱的。

举个栗子,某RD接到了这个需求,理清了逻辑,写好单测之后开始写代码(或者写好代码之后开始写单测)。但是由于手抖,将第三行的 && 写成了 ||:

public Integer fun4(Integer a, Integer b, Integer x) {

        if (a > 1 || b == 0) {
            x += a;
        }
        if (a == 2 || x > 1) {
            x += 1;
        }
        return x;
    }

然后跑一下单测,发现很顺滑,一下就过了。

随后该RD很高兴的将代码发布到了线上,结果就发生了严重的生产故障,于是该RD就被开除了。

行覆盖是一个最基础的覆盖方式,但是也是最薄弱的,如果完全依赖行覆盖,那不小心就会被开除。

2、判定覆盖 / 分支覆盖 (Decision Coverage/Branch Coverage)

public Integer fun3(Integer a, Integer b, Integer x) {

        if (a > 1 && b == 0) {
            x = x + a;
        }
        if (a == 2 || x > 1) {
            x += 1;
        }
        return x;
    }

判定覆盖的含义就是代码里每一个判定都要走一次true,一次false。依然用上面的代码,想要实现判定覆盖,需要以下case


a


b


x


预期结果


TC2


2


0


1


4


TC3


3


1


1


1

@Test
    public void testFun3DecisionCoverage(){
        Integer res = demoService.fun3(2,0,1);
        Assert.assertEquals(4,res.intValue());
        res = demoService.fun3(3,1,1);
        Assert.assertEquals(1,res.intValue());
    }

这两个用例可以保证判定 A: (a > 1 || b == 0)  和判定B: (a == 2 || x > 1) 分别都取一次true 和false:

tc2 时, A,B均为true;tc3时,A,B均为false。

可以看出分支覆盖依然有明显缺陷,并没有覆盖到  A: true  B: false 和 A:false B:true的情况。

3、条件覆盖 Condition Coverage

public Integer fun3(Integer a, Integer b, Integer x) {

        if (a > 1 && b == 0) {
            x = x + a;
        }
        if (a == 2 || x > 1) {
            x += 1;
        }
        return x;
    }

条件覆盖和判定覆盖类似,不过判定覆盖着眼于整个判定语句,而条件覆盖则着眼于某个判断条件。

条件覆盖需要保证每个判断条件的true false都要覆盖到,而不是整个判定语句。

例如,判定A (a > 1 || b == 0) ,只需要整个判定表达式分别取一次真假即可满足判定覆盖,而要满足条件覆盖,则需要判断条件 (a>1) 和 (b==0) 分别都取一次true false才算满足。

依然采用同样的代码,要想实现条件覆盖,则需要:


a


b


x


预期结果


TC4


2


0


3


6


TC5


0


1


0


0

@Test
    public void testFun3ConditionCoverage(){
        Integer res = demoService.fun3(2,0,3);
        Assert.assertEquals(6,res.intValue());
        res = demoService.fun3(0,1,0);
        Assert.assertEquals(0,res.intValue());
    }
 

这两个用例可以保证 (a > 1)   (b==0) (a == 2) (x > 1) 四个条件都分别取true false。

很明显可以发现,这玩意儿依然是不全面的,这个例子里条件覆盖和判定覆盖存在同样的问题,覆盖的不够全面。

4、路径覆盖 Path Coverage

public Integer fun3(Integer a, Integer b, Integer x) {

        if (a > 1 && b == 0) {
            x = x + a;
        }
        if (a == 2 || x > 1) {
            x += 1;
        }
        return x;
    }

路径覆盖这个顾名思义就是覆盖所有可能执行的路径。

为了方便理解,这里先把流程图画出来。

红色代表一段路径。

首先梳理所有路径:

路径1:1-->3-->5;

路径2:1-->2-->5;

路径3:1-->3-->4;

路径4:1-->2-->4;

路径覆盖就是需要设计用例,将所有的路径都走一遍。

设计以下用例:


a


b


x


预期结果


经过路径


TC6


0


1


0


0


1


TC7


3


0


-3


0


2


TC8


2


1


3


4


3


TC9


2


0


3


6


4

@Test
    public void testFun3PathCoverage(){
        Integer res = demoService.fun3(0,1,0);
        Assert.assertEquals(0,res.intValue());

        res = demoService.fun3(3,0,-3);
        Assert.assertEquals(0,res.intValue());

        res = demoService.fun3(2,1,3);
        Assert.assertEquals(4,res.intValue());

        res = demoService.fun3(2,0,3);
        Assert.assertEquals(6,res.intValue());

    }

总结

这是最常见的几种覆盖类型,行覆盖、判定覆盖、条件覆盖优缺点一致,都是较为简单,方便构造用例,并且能对程序质量有一定保证作用。

路径覆盖最完善,但是在一些复杂的场景里,会带来测试代码指数级增长的副作用,这个对于绝大多数人都是无法接受的。

一般情况下,行覆盖和判定覆盖同时做到已经是比较良好的单测代码了,jacoco的覆盖率统计也都是基于判定覆盖的。

实战部分(Mock)

mock是什么

事实上,理论和实际总是有差距的,单测也同样。

虽然单测的指导性理论知识非常完备,但是实际工作中往往遇到各种各样的障碍。最常见的,莫过于各种外部依赖了,这些外部依赖总是会阻碍我们书写自己的单测代码。这时候,我们就要用到mock了。

Mock这个单词的意思就是假的,模拟的。mock就是用一些特殊的代码,模拟一下外部依赖,将我们的测试代码与外部依赖解耦。

最直接的mock,就是重新写一个外部依赖的mock类,在里面返回mock的数据。测试的时候,将外部依赖手动替换成mock类。但是这样mock需要频繁修改代码,基本没什么实际价值。

为了解决mock外部依赖的需求,业界也出现了各种各样的mock框架。常见的有mockito、PowerMockito、Spock。

Spock:比较新的测试框架,基于groovy。优点:语法优雅清晰,缺点:groovy需要学习,没有经历时间的考验。

Mockito:最常用,最可靠的测试框架,优点就是没有什么太大的缺点。

PowerMock:为了解决Mockito等基于cglib的框架无法mock 私有、静态方法而产生的框架。

这里都采用Mockito来作为使用的测试框架。

简单Mock实战

整体代码详见:https://github.com/csonezp/mockdemo

这里就写一些关键代码。

service是最需要关注的地方,重要的逻辑一般都在这里;同时又有诸多的外部依赖,用这一层做实际mock的实例是最合适的。

/**
 * @author zhangpeng34
 * Created on 2019/1/18 下午9:15
**/
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserDao userDao;

    @Override
    public User findUserById(Long id) {
        return userDao.findById(id).orElse(null);
    }

    @Override
    public User addUser(String name) {
        if(StringUtils.isEmpty(name)){
            return null;
        }
        User user = new User();
        user.setName(name);
        return userDao.save(user);
    }
}
 
 

这个service很简单,这里针对里面的addUser方法写一些对应的单测:

/**
 *
 * 单元测试,测试的目的是对java代码逻辑进行测试。
 * 单纯的逻辑测试,不应该加载外部依赖,所有的外部依赖应该mock掉,只关注本身逻辑。
 * 例如,需要测试service层时,所依赖的dao等,应提前mock掉,设置好测试需要的输入和输出即可。
 * dao层的逻辑应由dao层的测试保证,service层默认dao层是正确的。
**/
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTests {
    //mock注解创建一个被mock的实例
    @Mock
    UserDao userDao;

    //InjectMocks代表创建一个实例,其他带mock注解的示例将被注入到该实例用。
    //可以用该注解创建要被测试的实例,将实例所需的依赖用mock注解创建,即可mock掉依赖
    @InjectMocks
    UserServiceImpl UserServiceImpl;

    String addUserName = "testAddUser";

    /**
     * 初始化时设置一些需要mock的方法和返回值
     * 这里的设置表示碰到userDao的save方法,且参数为任一User类的实例时,返回提前预设的值
     */
    @Before
    public void init(){
        User user =new User();
        user.setId(1L);
        user.setName(addUserName);
        Mockito.when(userDao.save(any(User.class)))
                .thenReturn(user);
    }
    //正向流程
    @Test
    public void testAddUser(){
        User user = UserServiceImpl.addUser(addUserName);
        Assert.assertEquals(addUserName,user.getName());
        Assert.assertEquals(1L,user.getId().longValue());
    }
    //异常分支,name为null
    @Test
    public void testAddUserNull(){
        User user = UserServiceImpl.addUser(null);
        Assert.assertEquals(null,user);
    }
    //将各个分支都写出test和assert
    //............
}

集成测试

上面所说的都是单元测试,但是实际开发中,我们往往不光需要单元测试(甚至不需要单元测试。。。。),还需要有集成测试,来测试我们程序的整体运行情况。

集成测试并不是联调,集成测试用依然可以mock第三方依赖。

在我们的工程里,一般只要实际启动整个spring容器的测试代码,都是集成测试。

Controller层是较为适合集成测试的地方,这里用Controller层来做集成测试的示例。

@RestController
public class UserController {

    @Autowired
    UserService userService;

    @PostMapping(value = "/user")
    public Object register(String name) {
        return userService.addUser(name);
    }

    @GetMapping(value = "/user/{userId}")
    public Object getUserInfo(@PathVariable Long userId) {
        User user = userService.findUserById(userId);
        if (user != null) {
            return user;
        }
        return "fail";

    }

    @PostMapping(value = "/user/login")
    public Object login(Long id, String pwd) {
        User user =  userService.findUserById(id);
        if(user!=null){
            return user;
        }
        return "fail";
    }
}
 
 

下面写一个这次集成测试用的spring测试文件,位置如下:

测试代码:

配置文件如下:

spring.profiles=it
server.port=9898

spring.h2.console.enabled=true
spring.h2.console.path=/h2
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

spring.datasource.max-wait=10000
spring.datasource.max-active=5
spring.datasource.test-on-borrow=true
spring.datasource.test-while-idle = true
spring.datasource.validation-query = SELECT 1

# jpa
spring.jpa.hibernate.ddl-auto=update
#spring.jpa.hibernate.ddl-auto= create-drop
spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
 

这个配置文件的主要作用就是将程序连接的DB换成一个内存数据库h2,这样就不用在测试时受DB的掣肘了。

@RunWith(SpringRunner.class)
@SpringBootTest
@ActiveProfiles("it")
@AutoConfigureMockMvc
public class UserControllerTests {
    @Autowired
    private MockMvc mvc;

    @Autowired
    UserDao userDao;

    Long userId;

    @Before
    public void init(){
        User user = new User();
        user.setName("111");
        userId = userDao.save(user).getId();
        System.out.println(userId);
    }

    /**
     * 测试/user/{userId}
     * @throws Exception
     */
    @Test
    public void testGetUser() throws Exception {
        //success
        this.mvc.perform(get("/user/"+userId)).andExpect(status().isOk())
                .andExpect(content().json("{ \"id\": "+userId+", \"name\": \"111\" }"));
        //fail
        this.mvc.perform(get("/user/"+(userId+100))).andExpect(status().isOk())
                .andExpect(content().string("fail"));
    }

    /**
     * 测试login
     * @throws Exception
     */
    @Test
    public void exampleTest2() throws Exception {
        //success
        this.mvc.perform(post("/user/login").param("id",userId.toString()).param("pwd","11"))
                .andExpect(status().isOk())
                .andExpect(content().json("{ \"id\": "+userId+", \"name\": \"111\" }"));

        //fail
        this.mvc.perform(post("/user/login").param("id",userId.toString()+"11").param("pwd","11"))
                .andExpect(status().isOk())
                .andExpect(content().string("fail"));
    }
}
 

@ActiveProfiles("it") 这个注解就是制定本测试代码加载的配置文件,”it“指 文件名 application-XX.properties  中间的xx,spring会自动根据名称去加载对应的配置文件。

init() 方法就是在内存数据库中构造自己需要的数据,这是集成测试最常见的步骤。

后面的测试代码就不需要解释太多了。

原文地址:https://www.cnblogs.com/csonezp/p/11757967.html

时间: 2024-10-07 12:06:34

单元测试实战(四种覆盖详解、测试实例)的相关文章

LVS三种模式及常见的四种调度算法详解

LVS三种工作模式: 1. Virtual server via NAT(VS-NAT) 优点:集群中的物理服务器可以使用任何支持TCP/IP操作系统,物理服务器可以分配Internet的保留私有地址,只有负载均衡器需要一个合法的IP地址. 缺点:扩展性有限.当服务器节点(普通PC服务器)数据增长到20个或更多时,负载均衡器将成为整个系统的瓶颈,因为所有的请求包和应答包都需要经过负载均衡器再生.假使TCP包的平均长度是536字节的话,平均包再生延迟时间大约为60us(在Pentium处理器上计算

PHP页面间参数传递的四种方法详解

2016-04-16 定义page01.php和page02.php两个php文件,将page01中的内容想办法传递到page02,然后供我们继续使用.--------------------------------------------------------------------------------第一种:使用客户端浏览器的cookie.cookie很容易理解,就是一个临时文件,可以把它看成一个储藏室,浏览器在浏览的过程中记录一些信息,就暂时存放在这里.在page01中设置一个coo

AES加密的四种模式详解

对称加密和分组加密中的四种模式(ECB.CBC.CFB.OFB) 一. AES对称加密:                                                       AES加密                          分组 二. 分组密码的填充                                                    分组密码的填充 e.g.:                                          

Android中点击事件的四种写法详解

Android中点击事件的四种写法使用内部类实现点击事件使用匿名内部类实现点击事件让MainActivity实现View.OnClickListener接口通过布局文件中控件的属性 第一种方法:使用内部类基本步骤如下: 新建一个MyOnClickListener类并实现View.OnClickListener接口 重写View.OnClickListener接口中的OnClick(View view)方法 给Button绑定一个监听器,并监听一个点击事件示例代码如下: public class

java四种内部类详解

一般来说,有4中内部类:常规内部类.静态内部类.局部内部类.匿名内部类. 一.常规内部类:常规内部类没有用static修饰且定义在在外部类类体中.   1.常规内部类中的方法可以直接使用外部类的实例变量和实例方法.   2.在常规内部类中可以直接用内部类创建对象   3.代码如下: public class MyOuter {  private int x = 100; // 创建内部类  class MyInner {   private String y = "Hello!"; p

网络互联技术(四)-LSA的第四和第五种类型详解

LSA的第四和第五种类型详解 一.External LSA:第五种LSA 我们前面已经详细介绍了前面三种LSA,今天就接着介绍第四种和第五种LSA.因为理解ASBR Summary LSA--第四种LSA需要涉及External LSA的一些知识,所以我们先介绍第五种LSA,然后再回过头来看第四种LSA. External LSA由ASBR(Autonomous System Border Router,自治系统边界路由器,我们前面说过了OSPF网络就是一个自治系统)产生.它是用来通告OSPF网

实战NFS文件共享存储详解

实战NFS文件共享存储详解 对Linux有兴趣的朋友加入QQ群:476794643 在线交流 本文防盗链:http://zhang789.blog.51cto.com 目录 NFS简介 NFS工作流程 使用NFS的好处 NFS应用环境 NFS服务器安装 NFS客户端配置 实例:创建web共享 NFS简介 NFS(Network File System)即网络文件系统,Sun公司开发,是FreeBSD支持的文件系统中的一种,它允许网络中的计算机之间通过TCP/IP网络共享资源.在NFS的应用中,本

Java构造和解析Json数据的两种方法详解一

原文链接:http://www.cnblogs.com/lanxuezaipiao/archive/2013/05/23/3096001.html 在www.json.org上公布了很多Java下的json构造和解析工具,其中org.json和json-lib比较简单,两者使用上差不多但还是有些区别.下面首先介绍用json-lib构造和解析Json数据的方法示例. 用org.son构造和解析Json数据的方法详解请参见我下一篇博文:Java构造和解析Json数据的两种方法详解二 一.介绍 JSO

JAVA:23种设计模式详解(转)

设计模式(Design Patterns) ——可复用面向对象软件的基础 设计模式(Design pattern)是一套被反复使用.多数人知晓的.经过分类编目的.代码设计经验的总结.使用设计模式是为了可重用代码.让代码更容易被他人理解.保证代 码可靠性. 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样.项目中合理的运用 设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我