Java高级程序设计

测试

软件测试

“使用人工和自动手段来运行或测试某个系统的过程,其目的在于检验它是否满足规定的需求或弄清楚预期结果与实际结果之间的差别。”

测试过程

在软件开发中是一个很重要的方面,良好的测试可以在很大程度决定一个应用的命运。

单元测试

单元测试中的“单元”在Java中可以理解为某个类中的某一个方法,因此“单元测试”就是针对Java中某个类中的某一个方法中的逻辑代码进行验证即测试该方法是不是可以正常工作。

为什么需要进行单元测试

  • 单元测试的测试相对于集成测试的测试成本较低
  • 提高开发效率
  • 提升项目工程代码质量
  • 快速定位Bug

如何进行单元测试

  • 第三方测试工具(框架)
    • JUnit
    • TestNG
    • Spock
    • ...

测试本身不是Java语言层面的内容

JUnit

Download and Install

Plain-old JAR

Download the following JARs and add them to your test classpath:

Maven

Next time...

简单示例

public class Math {
    /**
     * 阶乘
     * @param n
     * @return
     */
    public int factorial(int n) throws Exception {
        if (n < 0) {
            throw new Exception("负数没有阶乘");
        } else if (n <= 1) {
            return 1;
        } else {
            return n * factorial(n - 1);
        }
    }

}

如何验证factorial()是对的?

编写一个“测试类”

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

public class MathTest {
    @Test
    public void testFactorial() throws Exception {
        assertEquals(120, new Math().factorial(5));
    }
}

java -cp ... org.junit.runner.JUnitCore MathTest

在IDE中写测试用例

Keeps the bar green to keep the code clean

解释

  • 导入了org.junit.Testorg.junit.Assert.*,后者是静态导入
  • testFactorial是在要测试的方法名factorial前加个test
  • 所有测试方法返回类型必须为void且无参数。
  • 一个测试方法之所以是个测试方法是因为@Test这个注解
  • assertEquals的作用是判断两个参数是否相等
  • JUnit4包含一堆assertXX方法,这些assertXX统称为断言

使用注解来定义测试规则

  • @Test:把一个方法标记为测试方法
  • @Before:每一个测试方法执行前自动调用一次
  • @After:每一个测试方法执行完自动调用一次
  • @BeforeClass:所有测试方法执行前执行一次,在测试类还没有实例化就已经被加载,所以用static修饰
  • @AfterClass:所有测试方法执行完执行一次,在测试类还没有实例化就已经被加载,所以用static修饰
  • @Ignore:暂不执行该测试方法

试验一下

public class AnnotationTest {
    public AnnotationTest() {
        System.out.println("构造方法");
    }
    @BeforeClass
    public static void setUpBeforeClass() {
        System.out.println("BeforeClass");
    }
    @AfterClass
    public static void tearDownAfterClass() {
        System.out.println("AfterClass");
    }
    @Before
    public void setUp() {
        System.out.println("Before");
    }
    @After
    public void tearDown() {
        System.out.println("After");
    }
    @Test
    public void test1() {
        System.out.println("test1");
    }
    @Test
    public void test2() {
        System.out.println("test2");
    }
    @Ignore
    public void test3() {
        System.out.println("test3");
    }
}

解释一下

  • @BeforeClass@AfterClass在类被实例化前(构造方法执行前)就被调用了,而且只执行一次,通常用来初始化和关闭资源
  • @Before@After和在每个@Test执行前后都会被执行一次
  • @Test标记一个方法为测试方法,被@Ignore标记的测试方法不会被执行
  • JUnit4为了保证每个测试方法都是单元测试,是独立的互不影响。所以每个测试方法执行前都会重新实例化测试类

@Test的属性

  • excepted 用来测试异常的
  • timeout 用来测试性能的

还是这个阶乘函数

public class Math {
    /**
     * 阶乘
     * @param n
     * @return
     */
    public int factorial(int n) throws Exception {
        if (n < 0) {
            throw new Exception("负数没有阶乘");
        } else if (n <= 1) {
            return 1;
        } else {
            return n * factorial(n - 1);
        }
    }
}

excepted测试异常

@Test(expected = Exception.class)
public void testFactorialException() throws Exception {
    new Math().factorial(-1);
    fail("factorial参数为负数没有抛出异常");
}

测试方法会检查是否抛出Exception异常(当然也可以检测是否抛出其它异常)。如果抛出了异常那么测试通过(因为你的预期就是传进负数会抛出异常)。没有抛出异常则测试不通过执行fail("factorial参数为负数没有抛出异常")

你最喜欢的排序算法

public class Sort {
    public void sort(int[] arr) {
        //冒泡排序
        for (int i = 0; i < arr.length - 1; i++) { //控制比较轮数
            for (int j = 0; j < arr.length - i - 1; j++) { //控制每轮的两两比较次数
                if (arr[j] > arr[j + 1]) {
                    int temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
}

timeout来测试性能

public class SortTest {
    @Test(timeout = 2000)
    public void testSort() throws Exception {
        int[] arr = new int[50000]; //数组长度为50000
        int arrLength = arr.length;
        //随机生成数组元素
        Random r = new Random();
        for (int i = 0; i < arrLength; i++) {
            arr[i] = r.nextInt(arrLength);
        }
        new Sort().sort(arr);
    }
}

JUnit 5

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>5.14.1</version>
    <scope>test</scope>
</dependency>

生命周期与嵌套测试

  • @BeforeEach/@AfterEach@BeforeAll/@AfterAll
  • @DisplayName:自定义用例名称,结合中文或场景描述提升可读性

参数化与动态测试

@ParameterizedTest
@CsvSource({"1,1", "2,4"}) //支持复杂参数
void square(int input, int expected) {
    assertEquals(expected, Math.square(input));
}

@TestFactory //可在运行时生成用例
Stream<DynamicTest> dynamicFactorial() {
    return Stream.of(0,1,5).map(n ->
        DynamicTest.dynamicTest("factorial " + n,
            () -> assertTrue(new Math().factorial(n) > 0)));
}

Mock

  • 核心目标:隔离被测对象,替代昂贵或不可控的真实依赖
  • 常用工具:Mockito、PowerMockito、MockK 等,可自动生成桩对象并记录交互
interface RemoteUserClient {
    UserDto fetch(String id);  // 实际实现中会发起 HTTP/gRPC 调用
}
class UserService {
    private final RemoteUserClient client;
    UserService(RemoteUserClient client) { this.client = client; }
    String getUserName(String id) {
        UserDto dto = client.fetch(id);
        return dto == null ? "unknown" : dto.name();
    }
} 

Mockito 模拟远程调用

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
    @Mock RemoteUserClient client;      // 远程依赖
    @InjectMocks UserService service;   // 被测类

    @Test
    void fetch_remote_user_success() {
        when(client.fetch("42")).thenReturn(new UserDto("42", "Alice")); //设定远程调用的期望返回

        assertEquals("Alice", service.getUserName("42"));

        verify(client).fetch("42");  // 断言确实发起过远程调用
    }
}

除了mock之外还有别的机制,统称为测试替身(Test Doubles)

  • Dummy:仅满足方法签名
  • Stub:返回固定数据,不验证交互
  • Fake:轻量实现(内存数据库、简化消息队列)
  • Mock:关注行为校验(调用次数、参数)
  • Spy:真实对象包裹监控,同时执行实际逻辑

Testcontainers:可重复的依赖环境(以 MySQL 为例)

@Testcontainers
class MysqlIntegrationTest {
    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    @Test
    void query_version() throws Exception {
        try (Connection conn = DriverManager.getConnection(
                 mysql.getJdbcUrl(), mysql.getUsername(), mysql.getPassword());
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT 1")) {
            assertTrue(rs.next());
        }
    }
}

属性化测试(Property-based Testing)

  • 代表库:QuickTheories、jqwik
  • 思路:描述不变量(如排序后仍保持元素集合),框架自动生成大量输入
@Property
void sorted_list_is_monotonic(@ForAll List<Integer> data) {
    List<Integer> sorted = sort(data);
    Assertions.assertThat(sorted).isSorted();
}
  • 优势:更容易触及边界条件,自动 shrink(最简失败用例)

Mutation Testing(变异测试)

  • 工具:PIT(Pitest)
  • 做法:对字节码做细粒度变异(条件翻转、常量修改),若测试未失败即说明断言不足
  • 产出:Mutation Score(存活变异体越少越好),补充纯覆盖率指标
mvn org.pitest:pitest-maven:mutationCoverage

测试金字塔

  • 单元测试:快、隔离、数量最多
  • 服务/组件测试:验证模块交互
  • 端到端/UI:少量关键路径,成本高