JUnit5支持参数化测试,即提供多组参数将同一个测试方法运行多次(每次执行前后均会触发@BeforeEach, @AfterEach),
即通过@ParameterizedTest
配合参数来源注解使用:
- @ValueSource(strings = {“1”, “2”})
- @EnumSource(EnumClass.class)
- @NullSource, @EmptySource, @NullAndEmptySource
- @CsvSource({“1,2”, “3,4”})
- @CsvFileSource(resources = “/demo.csv”, files = “src/test/resources/demo.csv”)
- @MethodSource(“方法名”)
- @ArgumentsSource(MyArgumentsProvider.class)
- 参数转换@ConvertWith(ToStringArgumentConverter.class), @JavaTimeConversionPattern(“dd.MM.yyyy”)
- 参数聚合ArgumentsAccessor, @AggregateWith(PersonAggregator.class)
简单示例
@ValueSource(strings = {"1", "2"})
@ParameterizedTest
//使用@ParameterizedTest就不要再同时使用@Test了,
//否则报错:ParameterResolutionException: No ParameterResolver registered
void parameterizedTest(String arg) {
log.info("ParameterizedTest: {}", arg);
}
自定义@JsonFileSource
在编写测试用例的过程中,例如mockMvc工具需要Json字符串参数,
通常我们会构造完参数对象后再通过Json工具序列化成字符串,
又或者直接定义Json字符串(痛苦的转义😭),如:param="{\“key\”:\“value\”}",
以上2种方式都不是很方便,
现在有了Junit5参数化测试,我们可以自定义Json参数来源,思路就是:
- 自定义json文件(支持多个json文件)用于存放参数
- 读取json文件的指定jsonPath并提取对应的json字符串
- jsonPath对应的json内容可以Array、Object,若为Array则对应多个参数(测试多次)
@JsonFileSource 具体实现如下:
import org.apiguardian.api.API;
import org.junit.jupiter.params.provider.ArgumentsSource;
import java.lang.annotation.*;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
/**
* Junit5参数化测试 - Json文件来源注解<br/>
* 注:需配合@ParameterizedTest + @JsonFileSource注解使用
*
* @author luohq
* @version 1.0.0
* @date 2022-01-04
*/
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(status = EXPERIMENTAL, since = "5.0")
@ArgumentsSource(JsonArgumentsProvider.class)
public @interface JsonFileSource {
/**
* 参数对象对应的jsonPath(默认读取root对象)<br/>
* jsonKey表达式示例如下:
* <ul>
* <li>root对象:$</li>
* <li>json对象 or json数组:$.dict.addDict</li>
* <li>json数组中的第0个对象:$.dict.addDict[0]</li>
* </ul>
*/
String jsonKey() default "$";
/**
* json参数来源文件(*.json)(默认测试资源路径下jsonSource.json)
*/
String resource() default "classpath:jsonSource.json";
/**
* json参数需要被转换的Java对象类型(默认String类型)<br/>
* 注:复杂类型(如嵌套泛型)并未实际测试,<br/>
* 如有特殊类型(如日期等)可默认转换成String参数后再由自定义Json工具进行反序列化
*/
Class typeClass() default String.class;
}
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.neusoft.oscoe.osmium.core.exception.bussiness.BusinessException;
import lombok.extern.slf4j.Slf4j;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.support.AnnotationConsumer;
import org.springframework.util.ResourceUtils;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Junit5参数化测试 - Json文件来源 - 解析器<br/>
* 注:需配合@ParameterizedTest + @JsonFileSource注解使用
*
* @author luohq
* @date 2022-01-04
* @version 1.0.0
*/
@Slf4j
class JsonArgumentsProvider implements ArgumentsProvider, AnnotationConsumer<JsonFileSource> {
/**
* 标注在测试方法上的@JsonFileSource注解
*/
private JsonFileSource annotation;
@Override
public void accept(JsonFileSource annotation) {
this.annotation = annotation;
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return this.parseJsonParams()
.stream()
.map(Arguments::of);
}
/**
* 解析@JsonFileSource.resource指定的Json文件,并提取Json参数列表
*
* @return Java参数列表
*/
private List<Object> parseJsonParams() {
try {
//读取json文件
DocumentContext ctx = JsonPath.parse(ResourceUtils.getFile(this.annotation.resource()));
//String jsonStr = ctx.read(this.annotation.jsonKey(), new TypeRef<String>() {});
//解析jsonKey对应的json对象
Object jsonEle = ctx.read(this.annotation.jsonKey());
if (null == jsonEle) {
return Collections.emptyList();
}
//转换jsonArray为对象列表
if (jsonEle instanceof JSONArray) {
JSONArray jsonArray = (JSONArray) jsonEle;
AtomicInteger atomIndex = new AtomicInteger(0);
return jsonArray.stream()
.map(jsonItem -> this.convertSingleJsonParam(jsonItem, ctx, atomIndex.getAndIncrement()))
.collect(Collectors.toList());
}
//转换单个json对象为对象列表
return Arrays.asList(this.convertSingleJsonParam(jsonEle, ctx, null));
} catch (Throwable ex) {
throw new BusinessException("convert jsonFileSource exception!", ex);
}
}
/**
* 解析单个Json对象为Java参数(支持解析为String和具体Java对象)
*
* @param jsonEle json对象
* @param ctx json上下文
* @param index json数组中当前jsonEle的索引
* @return Java参数对象
*/
private Object convertSingleJsonParam(Object jsonEle, DocumentContext ctx, Integer index) {
//转换json对象为Java字符串
if (this.annotation.typeClass().equals(String.class)) {
String jsonObjStr = JsonPath.parse(jsonEle).jsonString();
log.debug("jsonObjStr: {}", jsonObjStr);
return jsonObjStr;
}
//转换json对象为Java对象(特殊处理jsonArray索引读取)
String jsonKeyWithIndex = null != index ? String.format("%s[%d]", this.annotation.jsonKey(), index) : this.annotation.jsonKey();
Object jsonObj = ctx.read(jsonKeyWithIndex, this.annotation.typeClass());
log.debug("jsonObj: {}", jsonObj);
return jsonObj;
}
}
@JsonFileSource使用示例
(1)首先在src/test/resources自定义json参数文件,
例如jsonSource/biz.json,
如果服务比较多,也可以定义多个**.json文件,然后通过@JsonFileSource.resource来指定对应的json文件。
{
"addBiz": [
{
"bizCode": "biz1",
"bizDesc": "biz2 desc"
},
{
"bizCode": "biz2",
"bizDesc": "biz2 desc"
}
],
"updateBiz": {
"id": "111",
"bizCode": "biz2",
"bizDesc": "biz2 desc"
}
}
(3)实现测试用例代码
public class BizControllerTest extends BaseTest {
...
@ParameterizedTest
@JsonFileSource(jsonKey = "$.addBiz", resource = "classpath:jsonSource/biz.json")
void addBiz(String bizJson) throws Exception {
this.mockMvc.perform(post(API_BASE_PATH)
//application/json请求体
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding(UTF_8)
//具体请求Json参数
.content(bizJson))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
//判断响应体code为成功
.andExpect(jsonPath(JSON_PATH_CODE).value(SystemCodeEnum.SYS_200.getCode()));
}
@ParameterizedTest
@JsonFileSource(jsonKey = "$.updateBiz", resource = resource = "classpath:jsonSource/biz.json")
void updateBiz(String bizJson) throws Exception {
this.mockMvc.perform(put(API_BASE_PATH)
//application/json请求体
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding(UTF_8)
//具体请求Json参数
.content(bizJson))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
//判断响应体respCode为成功
.andExpect(jsonPath(JSON_PATH_CODE).value(SystemCodeEnum.SYS_200.getCode()));
}
}
接下来便可正常执行测试用例,
最终可以发现addBiz被执行2次,updateBiz被执行1次。