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绝对值得一试。