掌握基本的单元测试技术,能够根据给定的程序写出相应的测试案例
- 单元:代码的基本组成单位,比如方法、类、模块等。
- 测试:给定输入,检查输出是否符合要求。
- 自动化测试:无需人工干预,定期、不定期自动执行的测试任务。
单元测试:对软件基本组成单元进行的测试,其测试对象是软件设计的最小单位(模块或者类)
覆盖类型
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
参考: