6个月前 (10-30)  Java系列 |   抢沙发  66 
文章评分 0 次,平均分 0.0
[收起] 文章目录

JUnit 5 FormattedSource源码(https://github.com/mikemybytes/junit5-formatted-source)允许按照用户定义的格式,以可读的方式定义测试用例参数。因此,它可以用来提高测试的可读性。让我们看一个例子:

class CalculatorTest {
    
    private final Calculator calculator = new Calculator();

    @FormattedSourceTest(format = "{0} + {1} = {2}", lines = {
            "1 + 2 = 3",
            "3 + 4 = 7"
    })
    void calculatesSum(int a, int b, int expectedSum) {
        Assertions.assertEquals(expectedSum, calculator.sum(a, b));
    }
    
}

测试执行输出显示了使用FormattedSource的另一个好处-该库为我们定制显示的测试用例名称,而无需付出任何额外的努力:

calculatesSum(int, int, int) ✔
├─ 1 + 2 = 3 ✔
└─ 3 + 4 = 7 ✔

当然,它可以做的远不止这些

这篇文章不会详尽地解释FormattedSource的可能。这就是《用户指南》的初衷。相反,我想展示Lib可以解决什么问题,以及某些东西是如何在引擎下工作的。

为什么?

我想到的@CsvSource最令人惊讶的用法可能是使用多字符字符串作为字段分隔符。尽管从CSV格式的角度来看,这是值得怀疑的,但它的效果正如预期的那样:

@ParameterizedTest
@CsvSource(delimiterString = "maps to", textBlock = """
    'foo'    maps to  'bar'
    'junit'  maps to  'jupiter'
""")
void shouldMapPair(String input, String expected) {
    String actual = pairMapper.map(input);
    Assertions.assertEquals(expected, actual);
}

我发现这种定义测试用例的方法非常强大。如果它一次支持多个分隔符String,我们可以将其用于更复杂的格式,如:

<input> maps to <output> using rule <ruleId>

使用@CsvSource定义这样的测试从一开始就感觉有点麻烦。我没有进一步滥用它,而是决定创建一个替代方案。FormattedSource就是这样诞生的。

FormattedSource库允许以简单灵活的方式定义测试用例。通过指定格式字符串,我们定义了所有测试用例定义的外观。再加上一种相当轻松的处理额外空白的方法,这使我们能够专注于可读性。使用@CsvSource无法实现的目标已成为标准解决方案:

@FormattedSourceTest(format = "{0} maps to {1} using rule {2}", textBlock = """
    'foo'   maps to 'bar'     using rule 486
    'junit' maps to 'jupiter' using rule 44
""")
void mapsOneValueToAnother(String input, String mapped, int ruleId) {
    // ...
}

除了测试用例参数占位符之外,格式字符串没有特殊意义。换句话说,您每次都可以用最易读的方式定义它。您也可以定义自己的argumentPlaceholder,而不是通过它们的索引来引用参数(就像内置的@DisplayName注释一样):

@FormattedSourceTest(
    format = "replaces ? with ?", argumentPlaceholder = "?",
    textBlock = """
    replaces foo with bar
    """)
void replacesOneStringWithAnother(String toBeReplaced, String replacement) {
    // toBeReplaced == "foo", replacement == "bar"
}

两个注解

对于以前使用JUnit5参数化测试的人来说,使用一个@FormattedSourceTest注释可能会显得有些奇怪。@ParameterizedTest在哪里?它是如何操作显示的测试用例名称的?

这里没有魔法。我们的第一个例子也可以这样实现:

class CalculatorTest {
    
    private final Calculator calculator = new Calculator();

    @ParameterizedTest(name = "{0} + {1} = {2}")
    @FormattedSource(format = "{0} + {1} = {2}", lines = {
            "1 + 2 = 3",
            "3 + 4 = 7"
    })
    void calculatesSum(int a, int b, int expectedSum) {
        Assertions.assertEquals(expectedSum, calculator.sum(a, b));
    }
    
}

@FormattedSource它只是一个自定义的参数化测试参数源,您可以一直这样使用它。事实上,@FormattedSourceTest@FormattedSource注释共享相同的参数集,因此它们可以互换使用。

将两个相当标准的注释合并为一个注释的好处是它的表达性。因为格式字符串总是代表一个人类可读的字符串,所以它感觉就像是显示的测试用例名称的合适候选者。使用@FormattedSourceTest可以避免重复@ParameterizedTest#名称中的格式字符串。

这两个声明都支持内置@CsvSource的许多功能,比如使用Java 15+文本块(而不是行数组)声明测试用例,以及定义自己的null值。

什么时候

当以人类可读的方式表达测试用例时,使用FormattedSource可能是一个好主意。当参数的数量合理并且可以很容易地表示为相对较短的字符串时,您可以将其视为@CsvSource的竞争对手。

FormattedSource可以很好地测试映射器和编码器等映射代码。然后,我们可以在格式字符串中使用一个简单的箭头:

@FormattedSourceTest(
    format = "{0} -> {1}", 
    textBlock = """
        'foo' -> 'bar'
        'junit' -> 'jupiter'
        """
)
void mapsOneValueToAnother(String input, String expectedValue) { ... }

或者将其替换为更具描述性的字符串,如{0}映射到{1}。或者,我们可以让整个测试用例定义听起来像一个真实的句子:

@FormattedSourceTest(
    format = "encodes {0} seconds as {1}", 
    lines = {
        "encodes 15 seconds as 'PT15S'",
        "encodes 180 seconds as 'PT3M'",
        "encodes 172800 seconds as 'PT48H'"
    }
)
void encodesDurationAsIso8601(long seconds, String expected) { ... }

当然,我们不仅仅局限于映射代码和传递字符串作为参数。FormattedSource只是一个JUnit 5参数源,因此它可以从内置的参数转换中受益。这允许应用更具创造性的方法-例如,测试我们的HTTP API:

enum UserType { CUSTOMER, ADMIN }

@FormattedSourceTest(
    format = "{0} returns {1} for {2}",
    textBlock = """
        /api/items           returns 200 for CUSTOMER
        /api/items           returns 404 for ADMIN
        /api/admin/inventory returns 401 for CUSTOMER
        /api/admin/inventory returns 200 for ADMIN
        """
)
void protectsEndpointsBasedOnUserType(URI uri, int httpCode, UserType type) {
    User user = TestUserFactory.authenticated(type);
    testHttpClient.get(uri).returnsResponseCode(httpCode);
}

然而,这并不意味着你应该忘记其他的论据来源!当自变量或测试用例的数量很高时,引入可读格式可能不切实际。在这些情况下,@CsvSource和鲜为人知的@CsvFileSource注释可能是更好的选择。此外,当参数不能很容易地表示为字符串时,FormattedSource也没有帮助。

该代码已被组织成一个多模块Maven项目。如果没有Java平台模块系统(JPMS)的支持,这一点就不值得在这里提及。该库有两个子模块:

junit5-formatted-source-parent
+- junit5-formatted-source
|  +- src  (library code)
|  \- test (unit, classpath tests)
\- junit5-formatted-source-tests
   \- test ("integration", module path tests)

junit5格式的源测试包括所有验证注释行为的“集成”测试。这个Maven子模块有自己的module-info.java文件,显式导入com.mikemybytes.junit5.formated作为依赖项。因此,它允许我测试JPMS支持以及功能。它还作为一个在模块路径上运行时如何使用库的实例。

1.0.0版本仍然需要至少Java 11才能运行。尽管我宣传至少在最新的LTS版本上运行,但我也希望避免为后期采用者关闭大门。此外,坚持使用与Spring Boot 3.0和Quarkus 3.0相同的基线版本感觉是一个合理的想法。同时,junit5格式的源代码测试模块中需要Java17+,因此可以在那里测试文本块等较新的语言功能。

总结

FormattedSource库为使用JUnit5编写参数化测试带来了另一种风格。这是一种以人类可读的方式定义测试用例的简单但功能强大的方法。

最近的1.0.0版本旨在强调API的稳定性和整体项目的成熟度。然而,提供所有最初设想的功能并不能阻止进一步的增强。

原文链接:https://mikemybytes.com/2023/05/03/introducing-junit5-formattedsource/

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/2916.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册