软件测试Junit单元测试

Posted Dreamchaser追梦

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件测试Junit单元测试相关的知识,希望对你有一定的参考价值。

前言

这学期有软件测试的课,课程作业便是写一些测试工具的实验报告,这确实是一个不错的机会,毕竟作为全栈工程师,怎么会连测试都不知道呢(笑哭)。

本文简单讲述了单元测试和Java的单测工具junit,以及配合mokito框架的简单使用。另外,我会将其他测试相关的文章也放在这个系列。

一、单元测试

1.单元测试是什么?

借用一下百度百科的话,

单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。

这里有几个关键点:

  • 单元是人为规定的
  • 单元测试是独立单元,要和其他部分相分离。

2.为什么需要单元测试?

这里谈谈我自己的感受,就我自己而言,是没有单元测试的习惯的,我感觉单元测试会非常耗费时间,同时我认为这些时间花费的不太值得,因为在我初学阶段,做的都是一些简单的crud项目。
但是随着我开发的项目越来越大,需求越来越复杂,我渐渐发现我做的项目质量越来越不稳定,常常会出现一些奇怪的bug。当出现bug时,我们往往要定位问题的所在。就比如前段时间我自己做的一个通用物联网平台,在录制演示视频时输入矫正公式(支持四则运算),当时在其他设备下都能正常运作,但偏偏那次出现了异常。好在整个项目都是自己做的,对于一些实现细节上也都心里有数,调试了一会后发现是算法问题,那时我才猛然想起自己写的时候明白这个算法实现不支持负数,如果要进行负数运算得变成“(0-x)”的形式,而恰巧那台设备上传的数据是负数,所以出现了问题。

好在是全栈开发,所有东西都是自己做的,如果这个项目是团队开发,我估计定位bug的所耗费的时间将会指数级增长。

正因为在集成测试等大规模测试中,定位bug所耗费的时间实在是太长了,所以我们需要单元测试来保证每个小模块的正确性。 尽管它会耗费更多的时间,但是这些时间比起后期层出不穷的bug以及解决bug所耗费的是时间,这些都是值得的。

在开发项目的过程中,很多时候都是在解决之前的bug遗留。

我在网上看到过相关的总结,写的非常好分享一下——单元测试到底是什么?应该怎么做?

  • 单元测试对我们的产品质量是非常重要的。
  • 单元测试是所有测试中最底层的一类测试,是第一个环节,也是最重要的一个环节,是唯一一次有保证能够代码覆盖率达到100%的测试,是整个软件测试过程的基础和前提,单元测试防止了开发的后期因bug过多而失控,单元测试的性价比是最好的。
  • 据统计,大约有80%的错误是在软件设计阶段引入的,并且修正一个软件错误所需的费用将随着软件生命期的进展而上升。错误发现的越晚,修复它的费用就越高,而且呈指数增长的趋势。
  • 作为编码人员,也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序这一点的人,其他任何人都无法做到这一点代码规范、优化,可测试性的代码
  • 放心重构
  • 自动化执行three-thousand times

二、Junit

1.什么是junit

JUnit是一个Java语言的单元测试框架。它由Kent Beck和Erich Gamma建立,逐渐成为源于Kent Beck的sUnit的xUnit家族中为最成功的一个。 JUnit有它自己的JUnit扩展生态圈。
目前junit已经发展到了junit5,相较于junit4有了很大的改变。JUnit5由来自三个不同子项目的几个不同模块组成。
JUnit 5=JUnit平台+JUnit Jupiter+JUnit Vintage

详见官网

注:junit基本上Java单元测试的主流,现今大多数Java项目都有junit的身影

2.Junit概念——断言

刚接触过单元测试的同学在学习junit时肯定会疑惑assert方法到底是什么意思,什么叫断言。我一开始接触时就是这样,疑惑断言是干嘛的。

其实断言其实是一些辅助函数,他们用来帮助我们确定被测试的方法是否按照预期的效果正常工作,通常,把这些辅助函数称为断言。

3.Junit的简单使用

以下演示为maven项目

①导入依赖

<dependencies>
    <!-- ... -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.8.1</version>
        <scope>test</scope>
    </dependency>
    <!-- ... -->
</dependencies>
<build>
    <plugins>
        <plugin>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
        <plugin>
            <artifactId>maven-failsafe-plugin</artifactId>
            <version>2.22.2</version>
        </plugin>
    </plugins>
</build>

②编写测试用例

这里引用官网上的例子

import static org.junit.jupiter.api.Assertions.assertEquals;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class MyFirstJUnitJupiterTests {

    private final Calculator calculator = new Calculator();

    @Test
    void addition() {
        assertEquals(2, calculator.add(1, 1));
    }

}

上面这个例子就是断言了calculator.add(1, 1)的返回值会等于2。

在idea中运行测试将会很方便,只需点击运行图标即可

如果不是idea中,也只需加个mian函数运行即可。

如果运行断言正确,那么程序会如下:

如果断言错误,junit会给你抛出一个AssertionFailedError异常,并告诉你出错的情况

4.SpringBoot环境下的junit使用

当然我们在实际开发中,比如在SpringBoot环境下开发,这时很多业务代码类都是被注入到Spring容器,而类之间又有其他注入类的依赖,像之前那样创建一个测试对象显然不现实。那有什么办法能解决这个问题呢?
下面我来介绍一下junit在SpringBoot+SSM项目中的使用。

①导入依赖

在SpringBoot中它将依赖进行整合,如果我们需要测试的相关依赖,只需引入对应的测试模块即可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.junit.vintage</groupId>
            <artifactId>junit-vintage-engine</artifactId>
        </exclusion>
    </exclusions>
</dependency>

②编写测试用例

测试对象类

package com.example.demo.service;

import org.springframework.stereotype.Component;

@Component
public class Junit5Test {
    public int add(int i,int j){
        System.out.println("-----------add被执行了---------------");
        return i+j;
    }
    public int doAdd(int i,int j){
        System.out.println("------------doAdd被执行了--------------");
        //被mock的函数会先执行,且只会执行一次
        System.out.println(add(i,j));
        return add(i,j);
    }
}

测试用例

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

//初始化一个spring的上下文,使其可以使用一些注入(junit5)。junit4会用runwith
@SpringBootTest
class Junit5TestTest {
    @Autowired
    Junit5Test junit5Test;
    //会初始化一次
    @BeforeAll
    static void init(){
        System.out.println("init");
    }
    //所有测试方法前都会执行一遍
    @BeforeEach
    void each(){
        System.out.println("each");
    }

    @Test
    void getDeviceStatistic() {
        Assertions.assertEquals(2,spyJunit5Test.doAdd(1,1));
    }
}

如果需要SpringBoot上下文环境只需在其上加个@SpringBootTest注解即可,当然在老项目中我们可能会看到@RunWith(SpringRunner.class)这种写法。前者是junit5的写法,后者是junit4的写法。

当我们需要spring容器中的测试对象时,我们只需正常注入即可。

@Autowired
Junit5Test junit5Test;

三、模拟数据——mockito框架的使用

1.mock

在实际开发进行单测时,我们测试对象很可能需要请求网络数据或者改变数据库,可是我们又不想让它去变化,这时我们可以使用mockito框架来对数据进行mock。

所谓的mock,就是指,如果我们写的代码依赖于某些对象,而这些对象又很难手动创建(即不知道如何初始化等,像HttpRequest等对象),那么就用一个虚拟的对象来测试。因为它传入的是一个class文件,所以static代码块还是会被运行,但构造函数,实例代码块都不会被执行

2.打桩Stub

所谓打桩Stub,就是用来提供测试时所需要的测试数据,因为是mock的对象,所以可能有些方法并不能知道返回值,因此我们需要去假定返回值。可以对各种交互设置相应的回应,即对方法设置调用返回值,使用when(…).thenReturn(…)和doReturn(…).when(…)。

比如:


//You can mock concrete classes, not only interfaces
 LinkedList mockedList = mock(LinkedList.class);
 
 //stubbing
 when(mockedList.get(0)).thenReturn("first");
 when(mockedList.get(1)).thenThrow(new RuntimeException());
  • doReturn().when()是无副作用的。打桩的同时不会执行方法。
  • when().thenReturn()是有副作用的,其副作用是指在打桩的同时会先执行一遍方法,这时可能会造成一定的副作用。

3.@MockBean和@SpyBean

当然在SpringBoot的环境下也可以直接@SpyBean和@MockBean注解来替代@Autowired的注入对象,这样就有了一个虚拟的对象。

@MockBean
如果仅使用@MockBean,会将修饰的对象mock掉,这样Junit5Test的add()方法就不再执行具体的细节,但是MockBean会将目标对象的所有方法全部mock,所以test不能真实地被执行,也就无法测试了。

@SpyBean
而有些情况我们又需要执行真实的方法,我们只想对某些方法进行mock,这时就可以使用@SpyBean。
使用@SpyBean修饰的spyJunit5Test是一个真实对象,仅当when(spyJunit5Test.add(1,1)).thenReturn(2);时,add方法被打桩,其他的方法仍被真实调用。

以下是示例

package com.example.demo.service;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;

//初始化一个spring的上下文,使其可以使用一些注入(junit5)。junit4会用runwith
@SpringBootTest
class Junit5TestTest {
//    @Autowired
//    Junit5Test junit5Test;
    //介于@Autowired和@MockBean之间的注解,当配置了when时使用mock,没有进行打桩则走正常方法
    @SpyBean
    Junit5Test spyJunit5Test;
    //完全使用mock方法,方法都不会去真正执行。当调用mockBean修饰的方法时,不会去真正执行该方法,只会返回打桩后的值,如果没有打桩时会返回默认值,比如int就返回0。
//    @MockBean
//    Junit5Test mockJunit5Test;
    //会初始化一次
    @BeforeAll
    static void init(){
        System.out.println("init");
    }
    //所有测试方法前都会执行一遍
    @BeforeEach
    void each(){
        System.out.println("each");
    }

    @Test
    void getDeviceStatistic() {
        Assertions.assertEquals(1,1);
    }
    @Test
    void testDeviceStatisticByMock() {

        //配置mock
        when(spyJunit5Test.add(1,1)).thenReturn(2);
        Assertions.assertEquals(2,spyJunit5Test.doAdd(1,1));
    }
}

最后,祝大家程序员节快乐!

以上是关于软件测试Junit单元测试的主要内容,如果未能解决你的问题,请参考以下文章

四则运算单元测试

JUnit单元测试

通过JUnit进行Android单元测试

JUnit单元测试

JUnit单元测试

Junit单元测试--01