软工个人技巧

掌握基本的单元测试技术,能够根据给定的程序写出相应的测试案例

  • 单元:代码的基本组成单位,比如方法、类、模块等。
  • 测试:给定输入,检查输出是否符合要求。
  • 自动化测试:无需人工干预,定期、不定期自动执行的测试任务。

单元测试:对软件基本组成单元进行的测试,其测试对象是软件设计的最小单位(模块或者类)

覆盖类型

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;
    }

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

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

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());

    }
@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
    //............
}

了解什么是回归测试

  • Regress 的英语定义是:return to a worse or less developed state,是倒退、退化、退步、回归的意思。
  • 如果一个模块或功能以前是正常工作的,但是在一个新的构建中出了问题,那么这 个模块就出现了一个“退步”(Regression),从正常工作的稳定状态退化(回归)到不正常工作的状态。
  • 针对一个Bug Fix,我们也要做Regression Test。目的是:

1. 验证新的代码的确改正了缺陷

2. 同时要验证新的代码有没有破坏模块的现有功能,有没有Regression

参考:

单元测试实战(四种覆盖详解、测试实例) - csonezp - 博客园 (cnblogs.com)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值