JUnit测试中的断言艺术:如何精准验证你的代码行为

209 阅读5分钟

在编写测试用例时,验证测试结果是否符合预期是至关重要的一步。JUnit 提供了强大的 Assertions 类,帮助我们轻松实现这一目标。通过断言,我们可以确保代码在各种情况下都能正确运行,从而提升代码的可靠性和稳定性。

验证布尔值:assertTrueassertFalse

在测试中,我们经常需要验证某个条件是否为 truefalse。使用 Assertions.assertTrueAssertions.assertFalse,可以轻松判断返回结果是否符合预期。如果结果不符合预期,断言会抛出异常,并附带自定义的错误信息,帮助你快速定位问题。

@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testAssertTrue(boolean isTrue) {
//        Assertions.assertTrue(isTrue);
    Assertions.assertTrue(isTrue, "返回结果必须是True");
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
public void testAssertFalse(boolean isFalse) {
//        Assertions.assertFalse(isFalse);
    Assertions.assertFalse(isFalse, "返回结果必须是false");
}

验证空值:assertNotNullassertNull

在处理对象时,我们经常需要判断某个对象是否为 nullAssertions.assertNotNullAssertions.assertNull 可以帮助我们验证对象是否为空。这在处理数据库查询、API 调用等场景时尤为有用。

@ParameterizedTest
@NullSource
@ValueSource(strings = {"A", "B", "C"})
public void testAssertNotNull(String name) {
//        Assertions.assertNotNull(name);
    Assertions.assertNotNull(name, "name字段必须不为null");
}
@ParameterizedTest
@NullSource
@ValueSource(strings = {"A", "B", "C"})
public void testAssertNull(String name) {
//        Assertions.assertNull(name);
    Assertions.assertNull(name, "name字段必须为null");
}

证相等性:assertEquals

无论是基本类型还是对象,Assertions.assertEquals 都可以帮助我们验证两个值是否相等。这个方法支持多种数据类型,包括 intlongfloatdoubleString 等。

@ParameterizedTest
@ValueSource(ints = {1, 127, 256, 1024})
public void testAssertEqInt(int num) {
    Assertions.assertEquals(num, Integer.valueOf("1024"));
}
​
@ParameterizedTest
@ValueSource(longs = {1L, 127L, 256L, 1024L})
public void testAssertEqLong(long num) {
    Assertions.assertEquals(num, 256);
}
​
@ParameterizedTest
@ValueSource(floats = {1f, 127f, 256f, 1024f})
public void testAssertEqLong(float num) {
    Assertions.assertEquals(num, 127f);
}
​
@ParameterizedTest
@ValueSource(doubles = {1d, 127d, 256d, 1024.13d})
public void testAssertEqLong(double num) {
    Assertions.assertEquals(num, 1024.13d);
}
​
@ParameterizedTest
@ValueSource(strings = {"a", "b", "c"})
public void testAssertEqString(String str) {
    Assertions.assertEquals(str, "b");
}
​
@ParameterizedTest
@ValueSource(chars = {'a', 'b', 'c'})
public void testAssertEqChar(char chars) {
    Assertions.assertEquals(chars, 'c');
}
​
@ParameterizedTest
@ValueSource(bytes = {-128, 0, 127})
public void testAssertEqBytes(Byte bytes) {
    Assertions.assertEquals(bytes, (byte)127, () -> "bytes: " + bytes + " != " + (byte)127);
}

验证数组相等性:assertArrayEquals

在处理数组时,Assertions.assertArrayEquals 可以帮助我们验证两个数组是否相等。它会逐个比较数组中的元素,确保它们完全一致。

static Stream<Arguments> createIntArr() {
    int[] arr1 = {1, 2, 3};
    int[] arr2 = {3, 4, 5};
    int[] arr3 = {5, 6, 7};
    return Stream.of(Arguments.of(arr1), Arguments.of(arr2), Arguments.of(arr3));
}
​
@ParameterizedTest
@MethodSource("createIntArr")
public void testAssertIntArr(int[] arr) {
    int[] tmp = {3, 4, 5};
    Assertions.assertArrayEquals(arr, tmp);
}

验证异常:assertThrowsassertDoesNotThrow

在测试中,我们不仅需要验证代码的正确执行,还需要确保代码在特定情况下抛出预期的异常。Assertions.assertThrows 可以帮助我们验证代码是否抛出了指定的异常,而 Assertions.assertDoesNotThrow 则可以确保代码在预期情况下不会抛出任何异常。

@ParameterizedTest
@ValueSource(ints = {-1, 0, 1})
public void testAssertThrow(int num) {
    ArithmeticException arithmeticException = Assertions.assertThrows(ArithmeticException.class, () -> {
        int res = 1 / num;
    });
​
    System.out.println(arithmeticException.getMessage());
}

检测当前代码运行预期不会出现异常AssertDoesNotThrow.assertDoesNotThrow,通过测试用例,否则不通过

@ParameterizedTest
@ValueSource(strings = {"/Users/jay/Desktop/users/spring-boot-test-junit/testdb.mv.db", "/Users/jay/Desktop/users" +
        "/spring-boot-test-junit/test.mv.db0", "/Users/jay/Desktop/users/spring-boot-test-junit/test.mv.db1"})
public void testAssertNotThrow(String path) {
    Assertions.assertDoesNotThrow(() -> {
        Path pathObj = Paths.get(path);
        byte[] bytes = Files.readAllBytes(pathObj);
​
        System.out.println(bytes.length);
    });
}

验证对象引用:assertSame

在某些情况下,我们需要验证两个对象是否是同一个实例。Assertions.assertSame 可以帮助我们验证两个对象是否指向同一个内存地址,这在单例模式的测试中尤为有用。

@ParameterizedTest
@ValueSource(strings = {"abc", "dcf", "s", "b", "e", "d", "fr", "gt", "lt", "qwe"})
public void testAssertSame(String str) {
    Random random = new Random();
    int i = random.nextInt();
    String tmp;
    if (i % 2 == 0) {
        tmp = str;
    } else {
        tmp = new String(str);
    }
    Assertions.assertSame(str, tmp);
}

检测是否是同一个对象实例,比如单例模式

static Stream<Arguments> createObj() {
    return Stream.of(Arguments.of(Color.RED.getRed()), Arguments.of(new Red()));
}
​
@ParameterizedTest
@MethodSource("createObj")
public void testAssertSameObj(Red red) {
​
    Assertions.assertSame(red, Color.RED.getRed());
}
​
static class Red {
​
}
​
​
enum Color {
    RED;
​
    private Red red;
​
    Color() {
        red = new Red();
    }
​
    public Red getRed() {
        return red;
    }
}

批量验证:assertAll

在复杂的测试场景中,我们可能需要对多个断言进行批量验证。Assertions.assertAll 允许我们一次性验证多个断言,即使其中某些断言失败,其他断言仍然会被执行。最终,所有失败的断言信息会一次性报告,帮助我们全面了解测试结果。

@Test
public void testAssertAll() {
    HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();
    Assertions.assertAll("批量测试接口是否正常可访问", () -> {
        HttpRequest requestV2ex =
                HttpRequest.newBuilder().uri(URI.create("https://v2ex.com/")).timeout(Duration.ofSeconds(120)).GET().build();
        Assertions.assertDoesNotThrow(() -> {
            HttpResponse<String> v2ex = client.send(requestV2ex, HttpResponse.BodyHandlers.ofString());
            System.out.println(v2ex);
            Assertions.assertEquals(v2ex.statusCode(), 200);
        });
    }, () -> {
        HttpRequest requestBaiDu = HttpRequest.newBuilder().uri(URI.create("https://www.baidu.com/")).GET().build();
        HttpResponse<String> baidu = client.send(requestBaiDu, HttpResponse.BodyHandlers.ofString());
        System.out.println(baidu);
        Assertions.assertEquals(baidu.statusCode(), 200);
    }, () -> {
        HttpRequest requestAlibaba =
                HttpRequest.newBuilder().uri(URI.create("https://www.alibaba.com/")).GET().build();
        HttpResponse<String> alibaba = client.send(requestAlibaba, HttpResponse.BodyHandlers.ofString());
        System.out.println(alibaba);
        Assertions.assertEquals(alibaba.statusCode(), 200);
    }, () -> {
        HttpRequest requestGoogle =
                HttpRequest.newBuilder().uri(URI.create("https://www.google.com/")).GET().build();
        HttpResponse<String> google = client.send(requestGoogle, HttpResponse.BodyHandlers.ofString());
        System.out.println(google);
        Assertions.assertEquals(google.statusCode(), 200);
    }, () -> {
        HttpRequest requestOther = HttpRequest.newBuilder().uri(URI.create("https://www.other.com/")).GET().build();
        SSLHandshakeException sslHandshakeException = Assertions.assertThrows(SSLHandshakeException.class, () -> {
            HttpResponse<String> other = client.send(requestOther, HttpResponse.BodyHandlers.ofString());
            System.out.println(other);
            Assertions.assertEquals(other.statusCode(), 200);
        });
​
        System.out.println(sslHandshakeException.getMessage());
    });
}

验证执行时间:assertTimeoutassertTimeoutPreemptively

在某些性能测试中,我们需要确保代码在规定的时间内完成执行。Assertions.assertTimeoutAssertions.assertTimeoutPreemptively 可以帮助我们验证代码的执行时间是否在预期范围内。后者在超时后会立即中断测试,避免测试无限卡住。

@ParameterizedTest
@ValueSource(strings = {"/Users/jay/Desktop/users/spring-boot-test-junit/testdb.mv.db", "/Users/jay/Downloads/hello-algo-1.0.0b6-zh-java.pdf", "/Users/jay/Downloads/Netty权威指南 第2版.pdf"})
public void testAssertTimeout(String filePath) {
    Assertions.assertTimeout(Duration.of(5, ChronoUnit.MILLIS), () -> {
        Path path = Paths.get(filePath);
​
        try (AsynchronousFileChannel asyncChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
​
            // 提交读取任务
            Future<Integer> result = asyncChannel.read(buffer, 0);
​
            // 等待读取完成
            while (!result.isDone()) {
                System.out.println("Reading...");
            }
​
            // 读取完成
            buffer.flip();
            System.out.println("File content:");
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
}

AssertTimeoutPreemptively 用于在测试中限制代码执行的时间。当代码的执行时间超过指定的时间限制时,测试会立即中断并失败。这在测试需要验证性能或时间约束的场景中非常有用。

强制中断执行:如果执行时间超过了 timeout 参数指定的限制,测试会立即失败,并抛出 TimeoutException。

预防死锁:在多线程或阻塞任务的测试中非常有用,可以避免代码死锁导致测试无限卡住。

线程级别的中断:使用独立线程运行可执行代码,超时后终止线程。

@ParameterizedTest
@ValueSource(strings = {"/Users/jay/Desktop/users/spring-boot-test-junit/testdb.mv.db", "/Users/jay/Downloads/hello-algo-1.0.0b6-zh-java.pdf", "/Users/jay/Downloads/Netty权威指南 第2版.pdf"})
public void testAssertTimeoutPreemptively(String filePath) {
    Assertions.assertTimeoutPreemptively(Duration.of(5, ChronoUnit.MILLIS), () -> {
        try (FileInputStream fis = new FileInputStream(filePath);
             FileChannel fileChannel = fis.getChannel()) {
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 读取文件内容
            while (fileChannel.read(buffer) > 0) {
                buffer.flip(); // 切换为读取模式
//                    while (buffer.hasRemaining()) {
//                        System.out.print((char) buffer.get()); // 输出缓冲区内容
//                    }
                buffer.clear(); // 清空缓冲区
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    });
}

条件跳过测试:assumingThat

在某些情况下,我们可能希望根据特定条件跳过某些测试用例。Assumptions.assumingThat 可以帮助我们实现这一需求,只有在满足特定条件时,测试用例才会被执行。

@ParameterizedTest
@ValueSource(strings = {"dev", "pre", "sit", "prod"})
public void testSkip(String active) {
    Assumptions.assumingThat(!active.equals("prod"), () -> {
        System.out.println( "在非prod环境运行这个测试用例");
    });
​
    System.out.println("无论是否存在 ENV 环境变量,这段代码都会执行");
}

image-20250103171911424.png