测试的粒度

单元测试:测试每一个单独的模块。

集成测试:测试模块之间的交互。

系统测试:由开发人员对整个系统进行测试。

验收测试:由客户根据用户需求对系统进行验证,通常没有正式的测试用例。

单元测试

对软件中的基本模块进行测试。

例如:

  • 一个函数
  • 一个类
  • 一个组件

单元测试通常能发现的问题:

  • 局部数据结构问题
  • 算法问题
  • 边界条件问题
  • 错误处理问题

为什么要进行单元测试?

采用 分而治之 的方法:

  • 将系统拆分为多个单元。
  • 分别调试每个单元。
  • 缩小 bug 可能存在的范围。
  • 不希望在其他单元中到处追踪 bug。

也就是说,单元测试的核心思想是:先保证小模块正确,再逐步构建更大的系统。

如何进行单元测试?

按层次构建系统:

  • 从不依赖其他类的类开始测试。
  • 然后在已经测试过的类的基础上,继续测试更高层的类。

好处:

  • 避免编写过多的 mock 类。
  • 当测试某个模块时,它依赖的模块已经比较可靠。

单元测试框架

  • xUnit
  • JUnit

测试程序

public class IMath {
 
    /**
     * 返回 x 的平方根的整数部分
     * 也就是舍弃小数部分
     */
    public int isqrt(int x) {
        int guess = 1;
        while (guess * guess < x) {
            guess++;
        }
        return guess;
    }
}

第一种测试写法

import org.junit.Test;
import static org.junit.Assert.*;
 
/** A JUnit test class to test the class IMath. */
public class IMathTestJUnit1 {
 
    /** A JUnit test method to test isqrt. */
    @Test
    public void testIsqrt() {
        IMath tester = new IMath();
 
        assertTrue(0 == tester.isqrt(0));
        assertTrue(1 == tester.isqrt(1));
        assertTrue(1 == tester.isqrt(2));
        assertTrue(1 == tester.isqrt(3));
        assertTrue(10 == tester.isqrt(100));
    }
 
    /** Other JUnit test methods */
}

这种写法还不够好,因为assertTrue(0 == tester.isqrt(0));只告诉JUnit一个布尔值,如果测试失败,JUnit很难给出详细 信息。

第二种写法

import org.junit.Test;
import static org.junit.Assert.*;
 
/** A JUnit test class to test the class IMath. */
public class IMathTestJUnit2 {
 
    /** A JUnit test method to test isqrt. */
    @Test
    public void testIsqrt() {
        IMath tester = new IMath();
 
        assertEquals(0, tester.isqrt(0));
        assertEquals(1, tester.isqrt(1));
        assertEquals(1, tester.isqrt(2));
        assertEquals(1, tester.isqrt(3));
        assertEquals(10, tester.isqrt(100));
    }
 
    /** Other JUnit test methods */
}

assertEquals(expected, actual)

还可以更好!

第三种写法

import org.junit.Test;
import static org.junit.Assert.*;
 
/** A JUnit test class to test the class IMath. */
public class IMathTestJUnit3 {
 
    /** A JUnit test method to test isqrt. */
    @Test
    public void testIsqrt() {
        IMath tester = new IMath();
 
        assertEquals("square root for 0 ", 0, tester.isqrt(0));
        assertEquals("square root for 1 ", 1, tester.isqrt(1));
        assertEquals("square root for 2 ", 1, tester.isqrt(2));
        assertEquals("square root for 3 ", 1, tester.isqrt(3));
        assertEquals("square root for 100 ", 10, tester.isqrt(100));
    }
 
    /** Other JUnit test methods */
}

assertEquals("提示信息", 期望值, 实际值);这样就更容易知道是哪一个输入出了问题。

然而还是有问题,如果有一个测试失败,后面的测试都不会进行

多个测试输入放在同一个测试方法中,失败信息不够完整,只能看到第一个失败点。

第四种写法

@Test
public void testIsqrt1() {
    assertEquals("square root for 0 ", 0, tester.isqrt(0));
}
 
@Test
public void testIsqrt2() {
    assertEquals("square root for 1 ", 1, tester.isqrt(1));
}
 
@Test
public void testIsqrt3() {
    assertEquals("square root for 2 ", 1, tester.isqrt(2));
}

可以看出,第四种写法还是有很多问题,我们写了很多相似的结构。

参数化测试

参数化测试的做法是:

只写一个测试逻辑 test m1(),然后把不同输入作为参数传进去。

同一个测试方法 + 多组测试数据。

@RunWith(Parameterized.class)
public class IMathTestJUnitParameterized {
    private IMath tester;
    private int input;
    private int expectedOutput;
 
    /** 构造方法:接收每一组输入-输出对 */
    public IMathTestJUnitParameterized(int input, int expectedOutput) {
        this.input = input;
        this.expectedOutput = expectedOutput;
    }
 
    @Before
    /** 创建测试夹具的初始化方法 */
    public void initialize() {
        tester = new IMath();
    }
 
    @Parameterized.Parameters
    /** 存储输入-输出对,也就是测试数据 */
    public static Collection<Object[]> valuePairs() {
        return Arrays.asList(new Object[][] {
            { 0, 0 },
            { 1, 1 },
            { 2, 1 },
            { 3, 1 },
            { 100, 10 }
        });
    }
 
    @Test
    /** 参数化的 JUnit 测试方法 */
    public void testIsqrt() {
        assertEquals(
            "square root for " + input + " ",
            expectedOutput,
            tester.isqrt(input)
        );
    }
}

但是,并不是所有测试都可以抽象成参数化测试。

参数化测试适用于相同测试逻辑 + 不同测试数据

但如果不同测试用例内部调用的方法顺序不同,或者测试流程本身不同,就不适合简单抽象成参数化测试。

例如:

public class ArrayList {
    ...
 
    /** 返回当前 list 的大小 */
    public int size() {
        ...
    }
 
    /** 向 list 中添加一个元素 */
    public void add(Object o) {
        ...
    }
 
    /** 从 list 中移除一个元素 */
    public void remove(int i) {
        ...
    }
}

JUnit 测试套件

一个测试套件是一组测试,或者是一组其他测试套件。

  • 将多个测试组织成一个更大的测试集合。
  • 帮助实现自动化测试。

断言

断言说明
fail([msg])使测试方法失败,msg 是可选提示信息
assertTrue([msg], bool)检查布尔条件是否为真
assertFalse([msg], bool)检查布尔条件是否为假
assertEquals([msg], expected, actual)检查期望值和实际值是否相等
assertNull([msg], obj)检查对象是否为 null
assertNotNull([msg], obj)检查对象是否不为 null
assertSame([msg], expected, actual)检查两个变量是否引用同一个对象
assertNotSame([msg], expected, actual)检查两个变量是否引用不同对象