浅谈单元测试 Junit4

在这里插入图片描述

JUint是Java编程语言的单元测试框架,用于编写和运行可重复的自动化测试。本文主要针对Junit4要点进行梳理总结。

一、浅谈单元测试

1.1 什么是单元测试

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

1.2 为什么要写单元测试

  这其实是问单元测试能带来什么好处,之所以把这个问题放在这里讨论,是因为需要清楚单元测试的使用场景,以及它做得到和做不到的。我们在写好一个函数或者类以后,需要检验我们的程序是否存在bug或者是否满足我们的需求,通常的做法就是将写好的函数在mian方法中调用,输入一些测试用例进行检验。当要检验的方法数量较少时,这种方法可行,但是当我们有大量的函数需要验证时,该方法就显得笨重且繁琐,往往需要我们人工检查输出的结果是否正确等,比较混乱。因此,单元测试就是用来解决这种繁琐问题的。使用单元测试可以有效地降低程序出错的机率,提供准确的文档,并帮助我们改进设计方案等等。以下列举了一些我为什么使用单元测试的好处:

  • 允许你对代码做出较任何改变,因为你了解单元测试会在你的预期之中。
  • 单元测试可以有效地降低程序出现BUG的机率。
  • 帮助你更深入地理解代码,因为在写单元测试的时候,你需要明确程序所有的执行流程及对应的执行结果等等。
  • 允许在任何时候代码重构,而不必担心破坏现有的代码,这使得我们编写程序更灵活。
  • 确保你的代码的健壮性,因为所有的测试都是通过了的。

1.3 什么时候写单元测试

写单元测试的时机不外乎三种情况:

  • 一是在具体实现代码之前,这是测试驱动开发(TDD)所提倡的;
  • 二是与具体实现代码同步进行。先写少量功能代码,紧接着写单元测试(重复这两个过程,直到完成功能代码开发)。其实这种方案跟第一种已经很接近,基本上功能代码开发完,单元测试也差不多完成了。
  • 三是编写完功能代码再写单元测试。我的实践经验告诉我,事后编写的单元测试“粒度”都比较粗。对同样的功能代码,采取前两种方案的结果可能是用10个“小”的单测来覆盖,每个单测比较简单易懂,可读性可维护性都比较好(重构时单测的改动不大);而第三种方案写的单测,往往是用1个“大”的单测来覆盖,这个单测逻辑就比较复杂,因为它要测的东西很多,可读性可维护性就比较差。

个人比较推荐单元测试与具体实现代码同步进行这个方案的,只有对需求有一定的理解后才能知道什么是代码的正确性,才能写出有效的单元测试来验证正确性,而能写出一些功能代码则说明对需求有一定理解了。

二、初识 Junit4

2.1 什么是JUnit

JUint是Java编程语言的单元测试框架,用于编写和运行可重复的自动化测试。其具有如下特点:

  • 提供注解来识别测试方法。
  • 提供断言来测试预期结果。
  • JUnit 测试允许你编写代码更快,并能提高质量。
  • JUnit 优雅简洁。没那么复杂,花费时间较少。
  • JUnit测试可以自动运行并且检查自身结果并提供即时反馈。所以也没有必要人工梳理测试结果的报告。
  • JUnit测试可以被组织为测试套件,包含测试用例,甚至其他的测试套件。
  • JUnit在一个条中显示进度。如果运行良好则是绿色;如果运行失败,则变成红色。

2.2 官方资料

最好的资料依然在Junit官方网站,以下我帮你总结下Junit相关的官方网址。

网址说明
https://junit.org/junit4/官网地址
https://junit.org/junit4/docs/current/user-guide/官方入门文档
https://github.com/junit-team官方github

2.3 常用注解

Junit4 注解提供了书写单元测试的基本功能,这里列出一些常用的注解,如下表所示:

注解说明
@Test测试注解,标记一个方法可以作为一个测试用例
@Ignore暂不执行该方法
@BeforeClass在所有测试之前,只执行一次,且必须为static void
@AfterClass在所有测试之后,只执行一次,且必须为 static void
@Before该方法必须在类中的每个测试之前执,以便执行某些必要的先决条件
@After该方法在每项测试后执行(如执行每一个测试后重置某些变量,删除临时变量等)
@Runwith测试类的初始化。在测试类的开头标注,表明运行环境
@Parameters指定测试类的测试数据集合
@FixMethodOrder指定测试方法的执行顺序

三、编写单元测试

3.1 引入相关依赖

  现在主流的IDE比如IDEA或者Eclipse都提供了对JUnit4的支持,可以非常方便的使用JUnit4。当你在代码中添加了@Test注解,然后使用IDE的自动补全功能时,一般情况下IDE会弹出对话框询问你是否将JUnit4库添加到项目的类路径下。当然也可以自己手动添加JUnit4的依赖。如果使用Maven,添加如下一段:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>org.example</groupId>
    <artifactId>MavenDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
</project>

3.2 编写测试类

  一个 JUnit 测试是一个在专用于测试的类中的一个方法, 并且这个方法被 @org.junit.Test 注解标注。我们只需要在我们要测试的方法上加上 @Test 注解,那么这个方法就会被当做一个单元测试,单独去运行,我们来试一下。

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class HelloWorldTest {
    @Test
    public void firstTest() {
        assertEquals(2, 1 + 1);
    }
}

@Test注解在方法上标记方法为测试方法,以便构建工具和 IDE 能够识别并执行它们。在 Junit 3中,是通过对测试类和测试方法的命名来确定是否是测试,且所有的测试类必须继承junit的测试基类。在 Junit 4中,定义一个测试方法变得简单很多,只需要在方法前加上@Test就行了。注意:测试方法必须是public void,即公共、无返回数据

3.3 测试生命周期

  JUnit4测试用例的完整的生命周期要经历几个阶段,他们分别是类级初始化资源处理、方法级初始化资源处理、执行测试用例中的方法、方法级销毁资源处理、类级销毁资源处理等。这几个阶段分别有对应的注解所标注,如下表

注解说明
@BeforeClass该方法只执行一次,并且在所有方法之前执行,例如创建数据库连接、读取文件等。
@AfterClass该方法只执行一次,并且在所有方法之后执行。通常用来对资源进行释放,比如数据库连接的关闭等。
@Before该方法在每一个测试方法之前运行,可以使用该方法进行初始化之类的操作
@After该方法在每一个测试方法之后运行,可以使用该方法进行释放资源,回收内存之类的操

简单来说,使用@BeforeClass 和 @AfterClass 两个注解标注的方法会在所有测试方法执行前后各执行一次,使用@Before 和 @After 两个注解标注的方法会在每个测试方法执行前后都执行一次。

import org.junit.*;

public class StandardTest {

    @BeforeClass
    public static void beforeClass() {
        System.out.println("@BeforeClass 方法被执行");
    }

    @AfterClass
    public static void afterClass() {
        System.out.println("@AfterClass 方法被执行");
    }

    @Before
    public void before() {
        System.out.println("@Before 单元测试开始前相关操作...");
    }

    @After
    public void after() {
        System.out.println("@After 单元测试结束后相关操作...");
    }

    @Test
    public void testCase1() {
        System.out.println("in test case 1");
    }

    @Test
    public void testCase2() {
        System.out.println("测试生命周期");
    }
}

这里有如下几点注意事项:

  • 父类的@BeforeClass注解方法会在子类的@BeforeClass注解方法执行前执行。
  • 父类@Before修饰的方法会在子类@Before修饰的方法执行前执行。
  • 父类@After修饰的方法会在子类@After修饰的方法执行后执行。
  • 父类中的被@AfterClass注解方法修饰的方法会在子类的@AfterClass注解修饰的方法执行之后才会被执行。

3.4 断言测试

断言测试注解如下表所示:

断言描述
void assertEquals()如果比较的两个对象是相等的,此方法将正常返回,否则测试将中止
void assertTrue()断言if条件或变量是否是true
void assertFalse()断言if条件或变量是否是false
void assertNotNull()断言一个对象不为空(null)
void assertNull()断言一个对象为空(null)
void assertSame()断言两个对象引用相同的对象
void assertNotSame()断言两个对象不是引用同一个对象
void assertArrayEquals()比较两个数组,如果它们相等,则该方法将继续进行不会发出错误,否则中止测试
import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;
import java.util.Arrays;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

public class AssertionTest {

    @Test
    public void testAssertArrayEquals() {
        byte[] expected = "trial".getBytes();
        byte[] actual = "trial".getBytes();
        assertArrayEquals("failure - byte arrays not same", expected, actual);
    }

    @Test
    public void testAssertEquals() {
        assertEquals("failure - strings are not equal", "text", "text");
    }

    @Test
    public void testAssertFalse() {
        assertFalse("failure - should be false", false);
    }

    @Test
    public void testAssertNotNull() {
        assertNotNull("should not be null", new Object());
    }

    @Test
    public void testAssertNotSame() {
        assertNotSame("should not be same Object", new Object(), new Object());
    }

    @Test
    public void testAssertNull() {
        assertNull("should be null", null);
    }

    @Test
    public void testAssertSame() {
        Integer aNumber = Integer.valueOf(768);
        assertSame("should be same", aNumber, aNumber);
    }

    @Test
    public void testAssertThatBothContainsString() {
        assertThat("albumen", both(containsString("a")).and(containsString("b")));
    }

    @Test
    public void testAssertThatHasItems() {
        assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
    }

    @Test
    public void testAssertThatEveryItemContainsString() {
        assertThat(Arrays.asList(new String[]{"fun", "ban", "net"}), everyItem(containsString("n")));
    }

    @Test
    public void testAssertThatHamcrestCoreMatchers() {
        assertThat("good", allOf(equalTo("good"), startsWith("good")));
        assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
        assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
        assertThat(7, not(CombinableMatcher.<Integer>either(equalTo(3)).or(equalTo(4))));
        assertThat(new Object(), not(sameInstance(new Object())));
    }

    @Test
    public void testAssertTrue() {
        assertTrue("failure - should be true", true);
    }
}

3.5 参数化测试

  为保证单元测试的严谨性,通常会模拟不同的测试数据来测试方法的处理能力,为此我们需要编写大量的单元测试的方法。可是这些测试方法都是大同小异的,它们的代码结构都是相同的,不同的仅仅是测试数据和期望值。为解决这个问题,Junit 4 引入了一个新的功能参数化测试,允许开发人员使用不同的值反复运行同一个测试。参数化测试主要解决一次性进行多个测试用例的测试。其主要思想是,将多个测试用例按照,{输入值,输出值}(输入值可以是多个)的列表方式进行测试。
  要进行参数化测试,需要在类上面指定如下的运行器:@RunWith (Parameterized.class),然后,在提供数据的方法上加上一个@Parameters注解,这个方法必须是静态static的,并且返回一个集合Collection。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.util.*;
import static org.junit.Assert.assertEquals;

@RunWith(Parameterized.class)
public class MyJUnit4ParaTest {
    private int input1;
    private int input2;
    private int expected;

    /**
     * 准备数据。数据的准备需要在一个方法中进行,该方法需要满足一定的要求:
     * 1)该方法必须由Parameters注解修饰
     * 2)该方法必须为public static的
     * 3)该方法必须返回Collection类型
     * 4)该方法的名字不做要求
     * 5)该方法没有参数
     */
    @Parameterized.Parameters
    public static Collection<Object[]> prepareData() {
        Object[][] object = {{3, 1, 4}, {36, 6, 42}, {0, 4, 4}};
        return Arrays.asList(object);
    }

    /**
     * 构造方法
     *
     * 必须要为类的所有字段赋值,不管是不是都用到!否则,Junit会出错。
     */
    public MyJUnit4ParaTest(int input1, int input2, int expected) {
        this.input1 = input1;
        this.input2 = input2;
        this.expected = expected;
    }

    @Test
    public void testDiv() {
        Calculator calc = new Calculator();
        // input1,input2,expected分别对应二维数组元素的第一个,第二个和第三个元素
        int result = calc.add(input1, input2);   
        assertEquals(expected, result);
    }
}

class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

  一般来说,在一个类里面只执行一个测试方法。因为所准备的数据是无法共用的,这就要求,所要测试的方法是大数据量的方法,所以才有必要写一个参数化测试。而在实际开发中,参数化测试用到的并不是特别多。

四、结语

  到这里,想必你对 JUnit 4 也有了基本的了解和掌握,都说单元测试是提升软件质量,提升研发效率的必备环节,从会用 JUnit 4 写单元测试开始,培养写测试代码的习惯,在不断实践中提升自身的开发效率,让写出来的代码有更质量的保证。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独泪了无痕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值