单元测试 JUnit4

173 阅读16分钟

转载自: 单元测试之Junit4详解

代码测试

  1. 按是否查看程序内部结构分为
  • 黑盒测试(black-box testing):只关心输入和输出的结果,黑盒测试分为功能测试和性能测试
  • 白盒测试(white-box testing):去研究里面的源代码和程序结构
  1. 按是否运行程序分为:静态测试、动态测试
  • 静态测试(static testing):是指不实际运行被测软件,而只是静态地检查程序代码、界面或文档可能存在的错误的过程。包括:
    • 对于代码测试,主要是测试代码是否符合相应的标准和规范。
    • 对于界面测试,主要测试软件的实际界面与需求中的说明是否相符。
    • 对于文档测试,主要测试用户手册和需求说明是否真正符合用户的实际需求。
  • 动态测试(dynamic testing):是指实际运行被测程序,输入相应的测试数据,检查输出结果和预期结果是否相符的过程
  1. 按阶段划分:
  • 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。
    桩模块(stud)是指模拟被测模块所调用的模块,驱动模块(driver)是指模拟被测模块的上级模块,驱动模块用来接收测试数据,启动被测模块并输出结果。
  • 集成测试(integration testing),是单元测试的下一阶段,是指将通过测试的单元模块组装成系统或子系统,再进行测试,重点测试不同模块的接口部门。
    集成测试就是用来检查各个单元模块结合到一起能否协同配合,正常运行。
  • 系统测试(system testing),指的是将整个软件系统看做一个整体进行测试,包括对功能、性能,以及软件所运行的软硬件环境进行测试。
    系统测试的主要依据是《系统需求规格说明书》文档
  • 验收测试(acceptance testing),指的是在系统测试的后期,以用户测试为主,或有测试人员等质量保障人员共同参与的测试,它也是软件正式交给用户使用的最后一道工序
    验收测试又分为a测试和beta测试,其中a测试指的是由用户、 测试人员、开发人员等共同参与的内部测试,而beta测试指的是内测后的公测,即完全交给最终用户测试

什么是单元测试

  • 维基百科定义简化版如下:
    • 单元测试 是针对 程序的最小单元 来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。一个单元可能是单个程序、类、对象、方法等。 ——维基百科
  • 单元测试的分类
    • 本地测试(Local tests): 只在本地机器JVM上运行,以最小化执行时间,这种单元测试不依赖于Android框架,或者即使有依赖,也很方便使用模拟框架来模拟依赖,以达到隔离Android依赖的目的,模拟框架如google推荐的[Mockito
    • 仪器化测试(Instrumented tests): 在真机或模拟器上运行的单元测试,由于需要跑到设备上,比较慢,这些测试可以访问仪器(Android系统)信息,比如被测应用程序的上下文,一般地,依赖不太方便通过模拟框架模拟时采用这种方式
  • 单元测试要测试什么
    • 列出想要测试覆盖的正常、异常情况,进行测试验证
    • 性能测试,例如某个算法的耗时等等

为什么要使用单元测试

  • 减少bug
  • 快速定位bug、减少调试时间
  • 提高代码质量
  • 放心重构
  • 主要是:领导要求、大牛都写单元测试、保住面子、心虚

Android中的测试

  • 单元测试(Junit4、Mockito、PowerMockito、Robolectric)
  • UI测试(Espresso、UI Automator)
  • 压力测试(Monkey)
  • 使用单元测试的时候注意进行依赖隔离
  • Anddroid中的单元测试框架有很多,Junit,Mockito,Robolectric,PowerMockito

注意

  • 这里提一句,测试用例用来达到也想要的结果,但对于逻辑错误无能为力
    • 两种错误:断言方法报错,逻辑代码问题

Junit介绍

  • 引用官网上的介绍:A programmer-oriented testing framework for Java.
    • 它是一个Java的面向程序员的测试框架
  • 了解 xUnit
    • xUnit是一套基于测试驱动开发的测试框架,包括了 PythonUint、CppUnit、Junit,分别是 Python、C++、Java的测试框架

使用

  • Junit 的功能十分的强大,知识点有很多,我这里讲一些常见的,剩下的大家自己下去了解

导入依赖

  • app/build 下导包

    testImplementation 'junit:junit:4.12' //前面是 testImplementation 1

基本使用,断言方法

  • 先准备好 原类(被测试类)

    • 注意,被测试的方法是要用 public 修饰,不然调用不到
    • 如下,下面就是几个简单的计算方法

    public class Calculator {

    public int add(int a,int b) {
        return a + b;
    }
    
    public int subtract(int a,int b) {
        return a - b;
    }
    
    public int multiply(int a,int b) {
        return a * b;
    }
    
    public int divide(int a,int b) {
        return a / b;
    }
    

    }

  • 鼠标光标移至 被测试类名 处,快捷键:Alt + Enter -> 选择 create Test (或者 右击 -> go to -> Test),然后进行选择一些类的方法、存放路径等信息,完成即可生成 测试类。测试类名是以 Test 作为后缀的,接下来就可以进行一些测试操作了

    • 关于 测试类 存放的位置
      • AS已经自动帮我们创建好了,如下:

    app/src ├── androidTestjava (仪器化单元测试、UI测试) ├── main/java (业务代码) └── test/java (本地单元测试)

  • 然后编写测试方法,使用 @Test 进行注解,都是 public void 且 无参数 的方法,这里编写测试方法有快捷键:Alt + Insert 即可选择自动生成测试方法等

    • 方法名与原类中的方法名一致,或以 test 为前缀进行命名
    • 使用 Assert 类中的静态方法 assertXxx() 进行测试,又称为断言方法
      • 下面代码中使用的是 assertEquals() 方法,比较实际值(调用 被测试方法的结果)与 期望值(你认为的值)是否相同。相同,Run 通过;不同,控制台提示错误信息(很明显的提示你)
    • 跑程序的时候可以选择是跑一个类(即全部的测试方法),还是指定的方法

    public class CalculatorTest {

    private static final String TAG = "TAG";
    private Calculator calculator = new Calculator();
    
    @Before
    public void setUp() throws Exception {
    

    // Log.d(TAG,"执行:setUp()"); }

    @After
    public void tearDown() throws Exception {
    

    // Log.d(TAG,"执行:tearDown()"); }

    @Test
    public void add() {
        assertEquals(6,calculator.add(3,3));
    }
    
    ···
    
    @Test
    public void divide() {
        assertEquals(3,calculator.divide(9,3));
    }
    

    }

测试套件

  • 测试套件就是组织测试类一起运行
  1. 写一个空类作为测试套件的入口类

  2. 使用 @RunWith 注解入口类,指明测试运行器为 Suite 类

    • Suite 是一个标准运行程序,允许您手动构建包含来自多个类的测试的套件
    • 作用就是改变测试运行器
  3. 使用 @Suite.SuiteClasses 注解入口类,并以数组的形式指明测试类

    • 这里,数组的先后顺序会影响进行测试的先后顺序
      • 按顺序依次执行方法,一个测试类的方法执行完之后才会执行下一个测试类的方法

    //测试套件:入口类 @RunWith(Suite.class) @Suite.SuiteClasses({Test1Test.class,Test2Test.class}) public class Test {

    }

    //测试套件:测试类一 public class Test1Test {

    @Test
    public void add() {
        assertEquals(5,new Test1().add(2,3));
        System.out.println("测试类Test1Test执行add()方法");
    }
    
    @Test
    public void subtract() {
        assertEquals(5,new Test1().subtract(5,0));
        System.out.println("测试类Test1Test执行subtract()方法");
    }
    

    }

    //测试套件:测试类二 public class Test2Test {

    @Test
    public void add() {
        assertEquals(5,new Test2().add(2,3));
        System.out.println("测试类Test2Test执行add()方法");
    }
    
    @Test
    public void subtract() {
        assertEquals(5,new Test2().subtract(6,1));
        System.out.println("测试类Test2Test执行subtract()方法");
    }
    

    }

参数化测试

  • 参数化测试允许开发人员使用 不同的值 反复运行 同一个测试

    • 和方法传值类似,一个方法可以调用多次,每次传递不同的值
    • 但是测试方法并没有参数,所以提供了这样一个参数化测试来实现
    • 使用如下
  • 使用 @RunWith 注解测试类,并指定属性值为 Parameterized.class

    • Parameterized是实现参数化测试的标准运行程序。运行参数化的测试类时,将为测试方法和测试数据元素的叉积创建实例。
  • 然后就是定义你的测试方法所需要的参数值,(期望值 + 断言方法里调用的方法所需的参数值)

  • 再编写一个构造方法,使用 @Parameterized.Parameters 进行注解,在构造方法中对定义的变量进行赋值

  • 再定义一个 public static 修饰的方法,返回值为 Collection<>,可以指定类型参数(推荐),也可以不指定

    • 在这个方法中定义一个二维数组(一维数组不够),然后使用 Arrays.asList() 方法将数组转换为 List集合,然后返回即可
      • 这里二维数组的元素就是你要进行测试的值
  • 至此,就可以正常的进行测试了

    @RunWith(Parameterized.class) public class ParamTestTest {

    private int expected = 0;
    private int input1 = 0;
    private int input2 = 0;
    
    @Parameterized.Parameters
    public static Collection<Object[]> t() {
        return Arrays.asList(new Object[][]{
                {5,2,3},
                {11,5,6}
        });
    }
    
    public ParamTestTest(int expected, int input1, int input2) {
        this.expected = expected;
        this.input1 = input1;
        this.input2 = input2;
    }
    
    @Test
    public void add() {
        assertEquals(expected,new ParamTest().add(input1,input2));
    }
    

    }

分类测试

  • 所谓分类测试,就是将测试代码中的方法进行分类,然后根据类别来选择该跑什么类别的方法,当然,这个时候同一类别的方法肯定是都会执行的

  • 主要是 @Category 注解的使用,还包含:@RunWith@SuiteClasses@IncludeCategory 这几个注解一起使用

  • 一共需要的类数量:原类 + 测试类 + 作标识符的类 + 最后运行的类

    • 原类:提供方法进行测试
    • 测试类:方法使用 @Category 注解,指明属性值(为类别的标识符),别忘了 @Test
    • 作标识符的类:就是几个空类
    • 最后运行的类:空类 + 注解
      • 使用 @RunWith 注解,并指定属性值为 Categories
      • 使用 @Categories.IncludeCategory 注解,并指明属性值为你要进行测试的 标识符的类
      • 使用 @Suite.SuiteClasses 注解,并指明属性值为测试类
    • 最后就可以运行了
  • 如下:

    //原类 public class CategoryTest {

    public int add(int a,int b) {
        return a + b;
    }
    
    public int subtract(int a,int b) {
        return a - b;
    }
    
    public int secondPower(int a) {
        return a * a;
    }
    
    public int theThirdPower(int a) {
        return a * a * a;
    }
    

    }

    //测试类 public class CategoryTestTest {

    @Category(BaseOperations.class)
    @Test
    public void add() {
        assertEquals(5,new CategoryTest().add(2,3));
    }
    
    @Category(BaseOperations.class)
    @Test
    public void subtract() {
        assertEquals(5,new CategoryTest().subtract(6,1));
    }
    
    @Category(PowerOperations.class)
    @Test
    public void secondPower() {
        assertEquals(4,new CategoryTest().secondPower(2));
    }
    
    @Category(PowerOperations.class)
    @Test
    public void theThirdPower() {
        assertEquals(8,new CategoryTest().theThirdPower(2));
    }
    

    }

    //作标识符进行判断种类的类(这里就是两种,幂运算,基本运算) public class PowerOperations { //这个代表幂运算类的方法标识符 }

    public class BaseOperations { //这个代表基本运算的方法标识符 }

    //最后运行的类 @RunWith(Categories.class) @Categories.IncludeCategory(BaseOperations.class) @Suite.SuiteClasses(CategoryTestTest.class) public class Operations { }

假设测试

  • 使用 assumeXxx(假设方法)进行判断,假设的条件是否成立,不成立则终止测试

  • Assume类中有很多的假设方法,可以自行查看,都有注释的

    @Test public void testAssumptions() { //假设进入testAssumptions时,变量i的值为10,如果该假设不满足,程序不会执行assumeThat后面的语句 assumeThat( i, is(10) ); //如果之前的假设成立,会打印"assumption is true!"到控制台,否则直接调出,执行下一个测试用例函数 System.out.println( "assumption is true!" );
    }

断言方法

  • 断言方法都有很多的重载形式,这里讲讲含义就行了,具体可以自行查看
  • 断言方法都来自 Assert 类,都是 static public 的
  • 不满足断言方法,则会抛出:AssertionError

assertArrayEquals

  • 断言两个对象数组相等

assertEquals

  • 断言两个对象相等

assertNotEquals()

  • 断言两个对象不相等

assertNull()

  • 断言一个对象为空

assertNotNull()

  • 断言一个对象不为空

assertSame()

  • 断言两个对象引用相同的对象

assertNotSame()

  • 断言两个对象没有引用同一对象

assertThat()

  • 断言实际值是否满足指定的条件,与 Matcher 一起使用,Matcher 指定条件
    • 详细看后面的讲解

assumeThat

  • 假设满足指定的条件,如果不满足,测试停止
  • Assume类中有很多的假设方法

assertTrue()

  • 断言条件为真

assertFalse()

  • 断言条件为假

assertThat与CoreMatchers

  • 断言实际值是否满足指定的条件,与 Matcher 一起使用

  • 这种断言方法一共两种重载形式:

    //第一个参数:reason 为断言失败时的输出信息 //第二个参数:actual 为断言的值或对象 //第三个参数:matcher 为断言的匹配器,里面的逻辑决定了 给定的 actual对象满不满足断言 public static void assertThat(String reason, T actual, Matcher<? super T> matcher) { MatcherAssert.assertThat(reason, actual, matcher); }

    public static void assertThat(T actual, Matcher<? super T> matcher) { assertThat("", actual, matcher); }

  • 在 CoreMatchers类中组织了所有JUnit内置的Matcher(匹配的方法),调用其任意一个方法都会创建一个与方法名字相关的Matcher

  • 下面的匹配并没有全部讲解完,具体请自行进入 org.hamcrest.CoreMatchers 类中查看

一般匹配符

  • allOf 匹配符表明如果接下来的所有条件必须都成立测试才通过,相当于“与”(&&)

    assertThat(testedNumber, allOf(is(8), not(16)));

  • anyOf 匹配符表明如果接下来的所有条件只要有一个成立则测试通过,相当于“或”(||)

    assertThat(testedNumber, anyOf(is(8), not(16)));

  • anything 匹配符表明无论什么条件,永远为true

    assertThat(testedNumber, anything());

  • is 匹配符表明如果前面待测的object等于后面给出的object,则测试通过

    assertThat(testedString, is("developerWorks"));

  • not 匹配符和 is 匹配符正好相反,表明如果前面待测的object不等于后面给出的object,则测试通过

    assertThat(testedString, not("developerWorks"));

  • both 匹配符表明当两个指定的匹配器都与检查的对象匹配时,该匹配器将匹配

    • 一般与 and 匹配符一起使用

    assertThat("两个并没有全部匹配",5,both(is(5)).and(not(4)));

字符串相关匹配符

  • containsString 匹配符表明如果测试的字符串testedString包含子字符串"developerWorks"则测试通过

    assertThat(testedString, containsString("dveloperWorks"));

  • endsWith 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"结尾则测试通过

    assertThat(testedString, endsWith("developerWorks"));

  • startsWith 匹配符表明如果测试的字符串testedString以子字符串"developerWorks"开始则测试通过

    assertThat(testedString, startsWith("developerWorks"));

  • equalTo 匹配符表明如果测试的testedValue等于expectedValue则测试通过,equalTo可以测试数值之间,字符串之间和对象之间是否相等,相当于Object的equals方法

    assertThat(testedValue, equalTo(expectedValue));

collection相关匹配符

  • hasItem 匹配符表明如果测试的迭代对象 iterableObject 含有元素 “element” 项则测试通过

    assertThat(iterableObject, hasItem("element"));

自定义 Matcher

  • 首先,大家要去看看 Matcher 这个接口与 BaseMatcher 这个实现类,就几个方法,比较简单,都写着有注释

  • 看完就知道我们自定义的 Matcher 应该去继承的是 BaseMatcher 这个类,而不是 Matcher 这个类

  • 使用:

    • 创建一个类继承 BaseMatcher 类,然后重写两个方法:matche()describeTo(),根据自己的需求去实现
    • 然后就可以在测试方法中使用了

    //IsRichardMatcher这个类是我实现的Matcher类 assertThat(user,new IsRichardMatcher());

注解

@Test

  • 将一个普通方法修饰为测试方法

    • 限时测试,异常捕获
  • 指定异常,以使测试方法在 当且仅当方法抛出 指定类的异常 时才成功,没有抛异常则会失败

    //在 @Test 中指定了 断言异常,运行时不会出现错误 @Test(expected = AssertionError.class) public void add() { assertEquals(6,calculator.add(3,1)); System.out.println("测试方法:add() 执行"); }

  • 以毫秒为单位 指定超时时间,超时则失败

    • 超过指定的时间会中断方法,并抛出异常

    @Test(timeout = 3000) public void subtract() { assertEquals(20,calculator.subtract(30,10)); System.out.println("测试方法:subtract() 执行"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } }

@Ignore

  • 在测试中测试运行器会忽略被 @Ignore 注解的方法,可以指定属性值(比如说为什么被忽略)

  • 一般用于测试方法还没有准备好,或者方法太耗时之类

    @Ignore("暂时不需要测试这个方法,所以进行忽略") @Test public void multiply() { assertEquals(10,calculator.multiply(2,5)); System.out.println("测试方法:multiply() 执行"); }

@RunWith

  • 可以更改运行测试器
  • JUnit将调用其引用的类,以在该类中运行测试,而不是JUnit内置的运行器。
    • JUnit 4中的套件是使用RunWith和一个名为Suite的自定义运行程序构建的
  • 一般在测试套件的时候使用

@BeforeClass

  • 在所有测试开始之前执行一次,必须为 static 修饰

  • 用于做一些耗时的初始化工作(如: 连接数据库)

    @BeforeClass public static void setUpBeforeClass() throws Exception { System.out.println("BeforeClass执行"); }

@AfterClass

  • 在所有测试结束之后执行一次,必须为 static 修饰

  • 用于清理数据(如: 断开数据连接)

    @AfterClass public static void tearDownAfterClass() throws Exception { System.out.println("AfterClass执行"); }

@Before

  • 在每个测试方法运行前执行一次

  • 用于准备测试环境(如: 初始化类,读输入流等),在一个测试类中,每个@Test方法的执行都会触发一次调用

    @Before public void setUp() throws Exception { System.out.println("Before执行"); }

@After

  • 在每个测试方法运行结束后执行一次

  • 这个方法在每个测试之后执行,用于清理测试环境数据,在一个测试类中,每个@Test方法的执行都会触发一次调用

    @After public void tearDown() throws Exception { System.out.println("After执行"); }

@FixMethodOrder

  • 修饰类,使得该测试类中的所有测试方法都按照方法名的字母顺序执行,属性值有三个

  • 指定形式:@FixMethodOrder(MethodSorters.NAME_ASCENDING),MethodSorters是一个枚举类,下面的三个值都是它的枚举常量

    • DEFAULT
      • 以确定但不可预测的顺序对测试方法进行排序,默认值
    • JVM
      • 将测试方法按JVM返回的顺序保留,请注意,JVM的运行顺序可能会有所不同
    • NAME_ASCENDING
      • 根据 方法名称 按字典名称 对测试方法进行排序,升序

    @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class CalculatorTest {

    ···
    
    @Test
    public void divide() {
        assertEquals(3,calculator.divide(9,3));
        System.out.println("测试方法:divide() 执行");
    }
    

    }

Junit 运行流程

  • 下面是打印出来的信息,代码很简单就贴出来了

    • 在开发中,BeforeClass、AfterClass注解的方法是 static 的

    BeforeClass执行(类加载) Before执行 测试方法:subtract() 执行 After执行 Before执行 测试方法:divide() 执行 After执行 Before执行 测试方法:add() 执行 After执行 Before执行 测试方法:multiply() 执行 After执行 AfterClass执行

  • 可以很明显的看出执行顺序:
    BeforeClass -> Before -> 测试方法 -> After -> AfterClass,如果有多个测试方法,那么重复: Before -> 测试方法 -> After 这段流程

使用中可能出现的 Bug

  1. 在测试方法中打 Log,出现错误:java.lang.RuntimeException: Method d in android.util.Log not mocked.
    a. 先仔细想一想,文章的一开始说了什么呀
    b. 就是 Junit 可以隔离 Android 框架呀

    @Before
    public void setUp() throws Exception {
       Log.d(TAG,"执行:setUp()");  //在此使用 Log
    }
    
  • 原因

    • Log类是android sdk的api,用Junit做单元测试,只能用纯java API,否则会报错
    • 这里就如果要使用 Log 就得自己创建一个 Log 类了,然后编写方法,如下:

    public class Log { public static int d(String tag, String msg) { System.out.println("DEBUG: " + tag + ": " + msg); return 0; }

    public static int i(String tag, String msg) {
        System.out.println("INFO: " + tag + ": " + msg);
        return 0;
    }
    
    public static int w(String tag, String msg) {
        System.out.println("WARN: " + tag + ": " + msg);
        return 0;
    }
    
    public static int e(String tag, String msg) {
        System.out.println("ERROR: " + tag + ": " + msg);
        return 0;
    }
    
    // add other methods if required...
    

    }