前言
本文主要介绍SkyWalking agent 插件自动化测试框架组成及测试流程,以及一个实际的自动化测试testcase。
SkyWalking 插件自动化测试框架介绍
相关文档:
这个自动化测试框架主要包含下面几个部分:
测试环境docker镜像
自动化测试脚本
testcase工程
结果验证工具
测试环境docker镜像
提供了两种测试环境:JVM-container 和 Tomcat-container,可以在创建testcase工程时选择使用的测试环境,官方推荐使用JVM-container。
其中JVM-container 可以理解为用于运行基于SpringBoot的testcase项目,包含启动脚本,可以修改启动的JVM参数,灵活性更好。
自动化测试脚本
主要使用到两个脚本:
创建testcase工程
${SKYWALKING_HOME}/test/plugin/generator.sh
执行脚本后,根据提示输入testcase的类型,名称等信息,脚本自动创建一个可以编译运行的样例项目。
运行测试案例
${SKYWALKING_HOME}/test/plugin/run.sh ${scenario_name}
${SKYWALKING_HOME}/test/plugin/run.sh -f ${scenario_name}
${SKYWALKING_HOME}/test/plugin/run.sh --debug ${scenario_name}
参数说明:
-f 参数强制重新创建镜像,在修改SkyWalking agent或plugin后需要添加-f参数,否则不能更新测试镜像中的agent程序。只改动testcase时不需要-f参数,减少启动时间。
--debug 启用调试模式,推荐使用此参数,可以保留测试过程的logs。
testcase工程
这里只介绍JVM-container类型的工程,实际上为基于SpringBoot的testcase应用。
[plugin-scenario]
|- [bin]
|- startup.sh
|- [config]
|- expectedData.yaml
|- [src]
|- [main]
|- ...
|- [resource]
|- log4j2.xml
|- pom.xml
|- configuration.yaml
|- support-version.list
[] = directory
工程文件说明:
文件/目录
说明
bin/startup.sh
testcase 应用启动脚本
config/expectedData.yaml
测试结果验证数据
configuration.yaml
testcase 配置,包含类型、启动脚本、检测url等
support-version.list
testcase支持的版本列表,默认为空不会进行检查,可以改为all表示全部
pom.xml
maven 项目描述文件
[src]
testcase 源码目录
其中对新手来说最难的是编写测试结果验证数据expectedData.yaml,数据格式不是很复杂,但要手写出来还是比较困难的。后面会提及一些技巧,可以从日志文件logs/validatolr.out中提取验证数据。
测试结果验证工具
SkyWalking 自动化测试工具的精髓所做应该就是自动验证测试结果数据,支持多种匹配条件表达式,可以灵活处理一些动态变化的数据。其中关键的是skywalking-validator-tools.jar工具,其源码repo为skywalking-agent-test-tool。
validator的代码量不大,通过阅读代码,可以了解expectedData.yaml的验证过程,理解验证数据的格式。
自动化测试流程
bash ./test/plugin/run.sh --debug xxxx-scenario
-> 准备测试的workspace
-> 编译testcase工程
-> 启动plugin-runner-helper 生成docker启动脚本等
-> scenario.sh
-> 启动测试环境docker实例
-> docker容器中执行 /run.sh
-> collector-startup.sh
-> 启动skywalking-mock-collector(测试数据收集服务)
-> testcase/bin/startup.sh
-> 启动testcase应用(-javaagent加载skywalking-agent.jar)
-> 循环healthCheck,等待testcase应用启动完毕
-> 访问entryService url,触发测试用例
-> 接收测试数据,写入到data/actualData.yaml文件
-> 启动skywalking-validator-tools.jar验证测试结果数据
-> 结束
设计测试用例
测试结果验证工具只能收集testcase应用的APM数据,比如span和logEvent等,不能收集http请求的返回内容,但可以收集到请求的状态码。
测试用例交互过程
这里仅介绍通过http请求交互,收集http相关数据,其它的数据与具体插件相关。比如测试redis apm插件时,可以收集到redis事件,包含执行的redis命令语句。
通过test/plugin/generator.sh命令生成测试用例中包含两个url,一个是healthCheck,一个是entryService。
1)healthCheck一般不需要管,用于探测testcase应用是否启动成功。如果编写的testcase有需要初始化的数据,请在healthCheck返回成功之前进行处理。
2)entryService是测试的入口url,healthCheck通过后,会接着访问entryService。可以在entryService的方法中进行调用测试方法,失败时返回4xx/5xx状态码。
这里要注意一个问题:
org.apache.skywalking.apm.testcase.*包下面的类不会被SkyWalking agent增强,这意味着这个包里面所有的类都不会被插件增强处理,比如标注了@Controller、@Component等的类并不会被apm-spring-annotation-plugin-*.jar 插件增强。如果要测试类增强的相关代码在testcase中,则要将代码放到这个包里面test.org.apache.skywalking.apm.testcase.*。
如何通过收集的APM数据判断测试成功或者失败?
1)http处理成功返回200/3xx时收集到span信息没有status_code,处理异常返回4xx/5xx错误时会产生一个tag记录status_code,可以用于验证区分测试结果。参考代码如下:
@RequestMapping("/dosomething")
public ResponseEntity dosomething() {
// check testcase is successful or not
if (isTestSuccess()) {
return ResponseEntity.ok("success");
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("failure");
}
}
2)还可以通过抛出异常来产生status_code和logEvent
@RequestMapping("/xxx-scenario")
@ResponseBody
public String testcase() throws HttpStatusCodeException {
if (isTestSuccess()) {
return "success";
}
throw new RuntimeException("failure");
}
编写测试结果验证数据(expectedData.yaml)
启用调试模式 (--debug),保留日志文件目录logs
bash ./test/plugin/run.sh --debug mytest-scenario
从日志文件提取收集的数据
日志文件目录:skywalking/test/plugin/workspace/mytest-scenario/all/logs
收集的数据可以从日志文件validatolr.out从提取到,找到后面的actual data:
[2020-06-10 07:31:56:674] [INFO] - org.apache.skywalking.plugin.test.agent.tool.validator.assertor.DataAssert.assertEquals(DataAssert.java:29) - actual data:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
....
}
]
}
]
}
将actual data的json数据转换为yaml
打开其他测试场景的expectedData.yaml,如httpclient-3.x-scenario/config/expectedData.yaml 的第一段内容:
segmentItems:
- serviceName: httpclient-3.x-scenario
segmentSize: ge 3
segments:
- segmentId: not null
spans:
- operationName: /httpclient-3.x-scenario/case/context-propagate
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/httpclient-3.x-scenario/case/context-propagate'}
- {key: http.method, value: GET}
refs:
- {parentEndpoint: /httpclient-3.x-scenario/case/httpclient, networkAddress: 'localhost:8080',
refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not
null, parentService: httpclient-3.x-scenario, traceId: not null}
skipAnalysis: 'false'
expectedData.yaml是用于检查测试结果的匹配模板,只有简单的几种匹配表达式如下表:
Operator for number
| Operator | Description |
| :--- | :--- |
| nq | Not equal |
| eq | Equal(default) |
| ge | Greater than or equal |
| gt | Greater than |
Operator for String
| Operator | Description |
| :--- | :--- |
| not null | Not null |
| null | Null or empty String |
| eq | Equal(default) |
比如segmentId是随机生成的,那么可以写成segmentId: not null 这样就可以匹配任意字符串。开始时间是变化的可以写成startTime: nq 0,只判断其是否大于0就可以。
对照获取到的actual data json,修改对应的字段就可以了。可以忽略检查healthCheck的数据,只需要写上关键的segment。看一个案例,actual data 如下:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
... healthCheck ...
},
{
"segmentId": "ab32f6a2774347958318b0fb06ccd2f0.33.15917743102950000",
"spans": [
{
"operationName": "/case/mytest-scenario",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/case/mytest-scenario"
},
{
"key": "http.method",
"value": "GET"
}
],
"startTime": "1591774310295",
"endTime": "1591774310316",
"componentId": "14",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
}
]
}
]
}
对应的expectedData.yaml(忽略检查healthCheck的数据):
segmentItems:
- serviceName: mytest-scenario
segmentSize: ge 1
segments:
- segmentId: not null
spans:
- operationName: /case/mytest-scenario
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: ge 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/case/mytest-scenario'}
- {key: http.method, value: GET}
skipAnalysis: 'false'
常见错误处理
docker 容器实例名冲突
./test/plugin/run.sh 出现下面的错误:
docker: Error response from daemon: Conflict. The container name "/xxxx-scenario-all-local" is already in use by container "42cdee17e557bb71...". You have to remove (or rename) that container to be able to reuse that name.
解决办法:
删除上次测试失败留下来的容器实例:docker rm xxxx-scenario-all-local
编写自动化测试testcase
1. 生成testcase工程
> cd skywalking
> bash ./test/plugin/generator.sh
Sets the scenario name
>: mytest-scenario
Chooses a type of container, 'jvm' or 'tomcat', which is 'jvm-container' or 'tomcat-container'
>: jvm
Gives an artifactId for your project (default: mytest-scenario)
>:
Sets the entry name of scenario (default: mytest-scenario)
>:
scenario_home: mytest-scenario
type: jvm
artifactId: mytest-scenario
scenario_case: mytest-scenario
Please confirm: [Y/N]
>: y
[INFO] Scanning for projects...
2. 修改配置文件
修改mytest-scenario/support-version.list,添加支持的版本,这里用全部版本all。注意,默认没有指定版本,不会启动测试场景。
# lists your version here
all
3. 编写测试用例
@RestController
@RequestMapping("/case")
public class CaseController {
private static final String SUCCESS = "Success";
@RequestMapping("/mytest-scenario")
@ResponseBody
public ResponseEntity testcase() {
//这里简单模拟,随机返回成功或者失败
SecureRandom random = new SecureRandom();
if (random.nextBoolean()) {
return ResponseEntity.ok(SUCCESS);
} else {
return ResponseEntity.notFound().build();
}
}
@RequestMapping("/healthCheck")
@ResponseBody
public String healthCheck() {
// your codes
return SUCCESS;
}
}
4. 本地测试testcase
bash ./test/plugin/run.sh --debug mytest-scenario
5. 提取测试收集的数据
从日志文件提取收集的数据,actual data部分。
日志文件:skywalking/test/plugin/workspace/mytest-scenario/all/logs/validatolr.out
[2020-06-10 09:00:03:655] [INFO] - org.apache.skywalking.plugin.test.agent.tool.validator.assertor.DataAssert.assertEquals(DataAssert.java:29) - actual data:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
"segmentId": "bfddda9bb70f49c694a90924b258a6da.32.15917795967760000",
"spans": [
{
"operationName": "/mytest-scenario/case/healthCheck",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/mytest-scenario/case/healthCheck"
},
{
"key": "http.method",
"value": "HEAD"
}
],
"startTime": "1591779596801",
"endTime": "1591779597069",
"componentId": "1",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
},
{
"segmentId": "bfddda9bb70f49c694a90924b258a6da.33.15917795971310000",
"spans": [
{
"operationName": "/mytest-scenario/case/mytest-scenario",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/mytest-scenario/case/mytest-scenario"
},
{
"key": "http.method",
"value": "GET"
}
],
"startTime": "1591779597132",
"endTime": "1591779597141",
"componentId": "1",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
}
]
}
]
}
6. 编写expectedData.yaml
segmentItems:
- serviceName: mytest-scenario
segmentSize: ge 1
segments:
- segmentId: not null
spans:
- operationName: /case/mytest-scenario
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: ge 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/case/mytest-scenario'}
- {key: http.method, value: GET}
skipAnalysis: 'false'