用Groovy写Spock测试

Spock框架依赖

插件

<plugin>
   <groupId>org.codehaus.gmavenplus</groupId>
   <artifactId>gmavenplus-plugin</artifactId>
   <version>1.13.1</version>
   <executions>
       <execution>
           <goals>
               <goal>addTestSources</goal>
               <goal>compileTests</goal>
           </goals>
       </execution>
   </executions>
</plugin>

依赖

spring boot版本大于2.5.x,若版本较低可以直接参考这个

<dependency>
   <groupId>org.codehaus.groovy</groupId>
   <artifactId>groovy-all</artifactId>
   <version>3.0.x</version>(或者更新版本)
   <type>pom</type>
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.spockframework</groupId>
   <artifactId>spock-core</artifactId>
   <version>2.3-groovy-3.0</version>(或者更新版本)
   <scope>test</scope>
</dependency>
<dependency>
   <groupId>org.spockframework</groupId>
   <artifactId>spock-spring</artifactId>
   <version>2.3-groovy-3.0</version>(或者更新版本,最好和上一个保持一致)
   <scope>test</scope>
</dependency>

说说优势

这里我们就要开始愉快的踩一捧一环节了!
相比于大家用的比较多的Mockito,Spock测试在动态类型、多参数测试等方面比Mockito好用的不知道到哪里去了!话不多说,直接上才艺:

1.多参数的测试

众所周知,业务代码事儿很多,需要根据情况返回不同的Error Code或者Response。我们可以想象这样一个例子:业务部门希望开发者增加上传文件的业务并提出了一系列要求,还说后面有可能会改需求,很经典,对吧?那么我们就需要参数可配置(即写在yaml里),我们根据这个需求做出了业务代码,并根据业务的要求在yaml写了这么一个file config:

	max-file-size: 100MB
    file-type-extensions:
      word: doc,docx
      outlook: msg
    valid-file-types: word,outlook
    invalid-file-name-characters: [ "..", "\\", ":", "*", "?", "\"", "<", ">", "|" ]

现在想要测试一下这个config里的检验函数好不好用。在我们用Mockito的时候怎么写呢,当参数只有一个的时候我们可以用@ParameterizedTest + @ValueSource来帮助我们

@ParameterizedTest
@ValueSource(strings = {"test1.doc", "test2.msg"})
void test_my_file_config(String fileName) {
    var fileTypes = Map.of("word", List.of("doc", "docx"), "outlook", List.of("msg"));
    var validFileTypes = List.of("word", "outlook");
    var invalidChars = List.of("..", "\\", ":", "*", "?", "\"", "<", ">", "|");
    var config = new FileConfig(
            DataSize.parse("100MB"),
            fileTypes,
            validFileTypes,
            invalidChars
    );

    Assertions.assertDoesNotThrow(() ->
            config.validate(new MockMultipartFile("test", fileName, null, new byte[1024])));
}

这看起来挺好的,对吧?但是测试内容并不满足于这一点,我们还需要测试Sad Path,根据不同的问题报出不同的错误码及错误信息,我们需要的参数变成了多个,此时Mockito怎么做呢?

@ParameterizedTest
@MethodSource({"valuesAndResults"})
void test_my_file_config(String fileName, String errCode, String errMsg) {
    var fileTypes = Map.of("word", List.of("doc", "docx"), "excel", List.of("xlsx"));
    var validFileTypes = List.of("word", "outlook");
    var invalidChars = List.of("..", "\\", ":", "*", "?", "\"", "<", ">", "|");
    var config = new FileConfig(
            DataSize.parse("100MB"),
            fileTypes,
            validFileTypes,
            invalidChars
    );

    var exception = Assertions.assertThrowsExactly(MyException.class, () ->
            config.validate(new MockMultipartFile("test", fileName, null, new byte[1024])));
    Assertions.assertEquals(errCode, exception.getCode());
    Assertions.assertEquals(errMsg, exception.getMessage());
    }
    
private static Stream<Arguments> valuesAndResults() {
    return Stream.of(
            Arguments.of("test1.xlsx", "errCode1", "errMsg1"),
            Arguments.of("test2..doc", "errCode2", "errMsg2"),
            Arguments.of("test3:.txt", "errCode3", "errMsg3")
    );
}

Mockito得用一个@MethodSource传一个method给测试方法,再在method里写好全部的参数,最终在测试方法里组装成我们需要的多参数测试,看起来也不赖,起码在我没接触Spock之前感觉它不赖,现在我们来看看Spock怎么解决它:

def "should succeed when validate file given file config"() {
        given:
        def fileConfig = new FileConfig(
                maxFileSize: DataSize.parse("100MB"),
                fileTypeExtensions: [
                        "word"   : ["doc", "docx"],
                        "outlook": ["msg"]],
                validFileTypes: ["word", "outlook"],
                invalidFileNameCharacters: ["..", "\\", ":", "*", "?", "\"", "<", ">", "|"]
        )

        when:
        fileConfig.validate(new MockMultipartFile("file", fileName, null, content))

        then:
        noExceptionThrown()

        where:
        fileName | content
        "test1.doc"  | new byte[1024]
        "test2.msg"  | new byte[1024]
    }

可以看到,Spock支持明文code写given: when: then:三个大块,且在given块内Spock支持使用key-value这种模式初始化对象,这种特点极大地节省了我们创造测试对象的时间,让我们写测试更丝滑(就像语法糖一样)。在带参测试的情况下,Spocck可以用where来提取参数,因此多参数下Spock也比Mockito方便了很多:

def "should failed when upload invalid file given wrong type and character"() {
        given:
        def fileConfig = new FileConfig(
                maxFileSize: DataSize.parse("100MB"),
                fileTypeExtensions: [
                        "word": ["doc", "docx"],
                        "text": ["txt"]
                ],
                validFileTypes: ["text"],
                invalidFileNameCharacters: ["..", "\\", ":", "*", "?", "\"", "<", ">", "|"]
        )

        when:
        fileConfig.validate(new MockMultipartFile("file", fileName, null, content))

        then:
        DSMException exception = thrown()
        exception.code == errCode
        exception.message == errMsg

        where:
        fileName      | content        || errCode | errMsg
        "unknown.doc" | new byte[1024] || 66666   | "some message1"
        "...txt"      | new byte[1024] || 88888   | "some message2"
        "a:b.txt"     | new byte[1024] || 88888   | "some message2"
        "a\\b.txt"    | new byte[1024] || 88888   | "some message2"
    }

2.执行次数验证

我们举个简单且fake的例子,有这样一个Service层的函数需要测试:

public void fakeMethod(String param) {
	var entity = new Fake();
	entity.setField2(param);
	List<Fake> res = fakeRepository.findAllByFake(entity);
	fakeRepository.setAllByFake(res);
}

我们想要测试这个函数它确实调用了Repository层的两个方法,Mockito要怎么写呢?

@Test
void test_fake_method() {
    //given
    var list = List.of(new Fake("1", "2"));
    Mockito.when(fakeRepository.findAllByFake(any())).thenReturn(list);

    //when
    fakeService.fakeMethod(anyString());

    //then
    Mockito.verify(fakeRepository, Mockito.times(1)).findAllByFake(any());
    Mockito.verify(fakeRepository, Mockito.times(1)).setAllByFake(anyList());
}

可以看到,因为Service层对Fake有一些“业务”操作,然后调用Repository层查找相应的对象并对这些业务操作进行了保存,所以为了能够确定代码真的调用了Repository层,我们需要验证Repository层的某些方法确实调用了多少次。看起来也不赖,对吧?那我们看看Spock怎么做的:

def "test fake method"() {
	given:
	1 * fakeRepository.findAllByFake(_ as Fake) >> List.of(new Fake(field1: "1", field2: "2"))

	when:
	fakeService.fakeMethod(_ as String)

	then:
	1 * fakeRepository.setAllByFake(_ as List)
}

Spock使用了num * method_name验证then块中的函数调用待测试函数的次数,而且它甚至可以测试given块中的函数!这代表了Spock在测试时用插桩以检测我们测试中想要测试的所有函数。此时我们回头看看Mockito的测试,只是为了这个简单的函数就得写那么长的verify测试代码,是不是看起来有点复杂了?

Spock和Mockito一样也可以完美的支持动态参数,让测试者关注在需要测试的函数上而非其上下文。相较于Mockito使用的any() / anyInt() / anyString()这样的函数,Spock表示的更加清晰。它使用下划线以接收任意一个参数,as关键字以限制其类型。如果as后的对象类型和测试时所传参数对不上也会报错。这些特点在上面的例子中已有展示了。

小结

纵使上面列举了那么多优点,Spock仍有其局限性:在单元测试中它的表现非常抢眼,但是到了集成测试甚至端到端测试它却并不好用。因此,各大项目的测试代码中仍是以Mockito为主,这正是因为Mockito可以很好地完成测试金字塔上的各级测试。但是如果项目上大量使用单元测试,Spock绝对值得一试。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值