DTeam 团队日志

Doer、Delivery、Dream

JUnit5 使用者:为何 Spock 值得你看它一眼

胡键 Posted at — Apr 15, 2020 阅读

这篇文章的诞生纯属巧合:一是早上看到微信群中有人问 Junit 的问题(往往这个时候我就忍不住向他们推销 Spock,同样的,这次也没有忍住),二是下午读到一篇转发的 JUnit5 vs Spock 的文章。于是乎,顺应天意写了下来。

我们团队已有多年(5+ 年)的 Spock 使用经验,不仅用在 Grails 项目中,而且在几乎所有 Java 项目中同样也采用它作为测试用例的书写工具。正如我在工具推荐给出的推荐原因:

Groovy DSL、jvm 下最好用的测试框架

Spock 完全担得起这样的评价!看看下面的代码示例,相信你有自己的判断。

阅读前的提醒:

好了,“ talk is cheap, show me the code ”。

测试结构

JUnit5

class SimpleCalculatorTest {
    @Test
    void shouldAddTwoNumbers() {
        //given
        Calculator calculator = new Calculator();
        //when
        int result = calculator.add(1, 2);
        //then
        assertEquals(3, result);
    }
}

Spock

class SimpleCalculatorSpec extends Specification {
    def "should add two numbers"() {
        given:
            Calculator calculator = new Calculator()
        when:
            int result = calculator.add(1, 2)
        then:
            result == 3
    }
}

异常测试

JUnit5

@Test
void shouldThrowBusinessExceptionOnCommunicationProblem() {
    //when
    Executable e = () -> client.sendPing(TEST_REQUEST_ID)
    //then
    CommunicationException thrown = assertThrows(CommunicationException.class, e);
    assertEquals("Communication problem when sending request with id: " + TEST_REQUEST_ID,
                 thrown.getMessage());
    assertEquals(TEST_REQUEST_ID, thrown.getRequestId());
}

Spock

你没看错,Spock 的测试方法名称可以是字符串,而且在我们的实际使用过程中直接就写成中文,这样产生出来的测试报告一眼就看明白什么问题。

def "should capture exception"() {
    when:
        client.sendPing(TEST_REQUEST_ID)
    then:
        CommunicationException e = thrown()
        e.message == "Communication problem when sending request with id: $TEST_REQUEST_ID"
        e.requestId == TEST_REQUEST_ID
}

有条件执行测试

JUnit5

结合相应的注解来做,这里就列出几例,其余自己去查文档。

@Test
@DisabledOnOs(OS.WINDOWS)
void shouldTestSymlinksBasedLogic() {
    ...
}

@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*32.*")
void shouldBeRunOn32BitSystems() {
    ...
}

Spock

利用 Groovy 动态语言的特性,使用“注解 + 闭包”的形式,提供更灵活的使用。

@IgnoreIf({ !jvm.java8Compatible })
def "should return empty Optional by default for unstubbed methods with Java 8+"() { ... }

@Requires({ sys["targetEnvironment"] != "prod" })
def "should execute smoke testing on non production environment"() { ... }

Mocking

JUnit5

需结合 Mockito

@Test
public void should_not_call_remote_service_if_found_in_cache() {
    //given
    given(cacheMock.getCachedOperator(CACHED_MOBILE_NUMBER)).willReturn(Optional.of(PLUS));
    //when
    service.checkOperator(CACHED_MOBILE_NUMBER);
    //then
    then(webserviceMock).should(never()).checkOperator(CACHED_MOBILE_NUMBER);
//   verify(webserviceMock, never()).checkOperator(CACHED_MOBILE_NUMBER);   //alternative syntax
}

Spock

内置了 Mock 机制

def "should not hit remote service if found in cache"() {
    given:
        cacheMock.getCachedOperator(CACHED_MOBILE_NUMBER) >> Optional.of(PLUS)
    when:
        service.checkOperator(CACHED_MOBILE_NUMBER)
    then:
        0 * webserviceMock.checkOperator(CACHED_MOBILE_NUMBER)
}

参数化

JUnit5

@ParameterizedTest(name = "value to pay for invoice {0} should be {1}")
@MethodSource("invoiceProvider")
void shouldCalculateToPayValueForInvoice(Invoice invoice, BigDecimal expectedValueToPay) {
    //when
    int valueToPay = invoice.toPayValue();
    //expect
    assertEquals(expectedValueToPay, valueToPay);
}

private static Stream<Arguments> invoiceProvider() {
    return Stream.of(
            Arguments.of(regularInvoice(), 54),
            Arguments.of(overduedInvoice(), 81),
            Arguments.of(paidInvoice(), 0)
    );
}

Spock

这是我的最爱

@Unroll
def "should sum two integers (#x + #y = #expectedResult)"() {
    when:
        int result = calculator.add(x, y)
    then:
        result == expectedResult
    where:
         x |  y || expectedResult
         1 |  2 ||  3
        -2 |  3 ||  1
        -1 | -2 || -3
}

并发测试

因为不用 JUnit 好多年,对于 JUnit5 没有去研究是否提供了对于并发测试编写的内部支持。

这里的例子来自于我之前写的 Spock + Vert.x 自动化测试的文章

when:
BlockingVariable<Integer> rowCount = new BlockingVariable<>()
BlockingVariable<String> callback = new BlockingVariable<>()
pgUtils.simpleSql(NamedQuery.uncalledCallback) { rowSet ->
    rowCount.set(rowSet.size())
    callback.set(rowSet.asList()[0].getString('callback'))
}

then:
rowCount.get() == 1
callback.get() == 'callback2'

几乎是无痛编写!

Spock 对于并发测试提供了若干辅助类:

最后

看完代码,你的答案是什么呢?