最近笔者在尝试基于应用日志来自动生成测试用例。这其中就需要一个配套的简易测试框架。梳理了一下,其中的技术点有: 0.使用csv文件来定义测试用例及步骤 1.使用自定义测试注解来定义测试用例(参考ZeroCode) 2.使用Junit5提供的extension机制来实现测试执行 3.使用简单工厂类提供执行驱动 4.使用OpenCsv来实现解析 5.使用Lombok来定义Java Bean 6.使用Junit5提供的参数化测试解决方案junit-jupiter-params来实现测试用例集
ZeroCode是一个轻量级的开源测试框架。它通过使用JSON或者YAML文件格式来定义测试用例,进而让测试用例的编写变得更为容易。 以下是其github项目首页提供的案例
@Test
@Scenario("test_customer_get_api.yml")
public void getCustomer_happyCase(){
// No code goes here. This remains empty.
}
其中test_customer_get_api.yml中就描述了这个接口测试用例的全部要素,具体如下:
---
url: api/v1/customers/123
method: GET
request:
headers:
Content-Type: application/json
retry:
max: 3
delay: 1000
verify:
status: 200
headers:
Content-Type:
- application/json; charset=utf-8
body:
id: 123
type: Premium Visa
addresses:
- type: Billing
line1: 10 Random St
verifyMode: LINIENT
这个YAML文件中包括了http接口测试中所需要的请求(含url、head、类型)以及返回、验证模式等内容,是一个不错的用例DSL。本身这是一个很好的开源测试框架,涵盖的测试类型也比较多,参与维护的人员和更新速度也不错。
A community-developed, free, open source, declarative API automation and load testing framework built using Java JUnit core runners for Http REST, SOAP, Security, Database, Kafka and much more. It enables to create and maintain test-cases with absolute ease.
在实际的测试过程中,对于文本格式的测试用例,往往有以下的需求:
为了实现上述需求,这就要求根据测试的特点,来定制一个类似的简易测试框架。
当设计一个自动化测试用例框架时,有一个很重要的三联问问题:
如何定义一个用例?如何定义用例的步骤?如何定义一个用例集?
在本案例中,我们约定
至于用csv文件来作为用例的载体,而不是json/yaml等更新的文件类型,或者xml/excel等传统文件类型,主要是基于以下两方面考虑
因此,如果以前述ZeroCode的接口为例,一个简单的接口自动化测试的用例格式可以是
num | type | url | params | response |
---|---|---|---|---|
1 | get | “api/v1/customers/123” | {"id": 123,"type": "Premium High Value","addresses": [{"type":"home","line1":"10 Random St"}]}" |
读者可能会问,那么head,content-type这些不要了么?status code 都等于200么?实际项目中经常用到的token怎么没有体现?等等问题。 这里我们假设,
以下是编写完成以后的一个测试用例的样例
package org.codefx.demo.junit5.extensions;
import org.junit.jupiter.api.Test;
import com.demo.junit5.Scenario;
class ScenarioTest {
@Test
@Scenario(value=".\\tests\\demo1\\sample.csv")
public void sampleTest() {
}
}
其中的sample.csv中的内容就是前述表格中的内容
[未完待续]
我们来看下测试用例中的注解的具体实现:
package com.demo.junit5;
import java.lang.annotation.*;
import org.junit.jupiter.api.extension.ExtendWith;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(ScenarioExtension.class)
public @interface Scenario {
String[] value() default "";
}
通过@Scenario 在某个方法上的注解,可以调用 @ExtendWith(ScenarioExtension.class) 中的具体功能。这也是JUnit5提供的一种回调机制,来扩展Junit5测试框架的功能。
JUnit5提供了非常友好的扩展性,最常用的是Before/After配套的一些Callback接口上,如下图所示:
这里我们就使用了一个BeforeTestExecutionCallback的接口来进行扩展,在被注解的用例执行之前,Junit5会首先调用该接口,实现自定义的功能。
package com.demo.junit5;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Iterator;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.util.AnnotationUtils;
import com.demo.junit5.bean.TestStep;
import com.demo.junit5.runner.Runner;
import com.demo.junit5.runner.RunnerFactory;
import com.opencsv.bean.CsvToBean;
import com.opencsv.bean.CsvToBeanBuilder;
class ScenarioExtension
implements BeforeTestExecutionCallback {
private Runner runner = RunnerFactory.getRunner("");
//实际项目中一般通过配置来传入具体的runner类型。这里只是一个Dummy样例。
// EXTENSION POINTS
public void beforeTestExecution(ExtensionContext context)
throws Exception{
Scenario scenario = AnnotationUtils.findAnnotation(
context.getRequiredTestMethod(), Scenario.class)
.orElse(null);
if (scenario == null) {
scenario = AnnotationUtils.findAnnotation(
context.getRequiredTestClass(), Scenario.class)
.orElse(null);
}
for(String v:scenario.value()) {
runCase(runner,v);
}
}
private static void runCase(Runner runner,String testCase)
throws IOException {
Reader reader = Files.newBufferedReader(Paths.get(testCase));
CsvToBean<TestStep> csvToBean = new CsvToBeanBuilder<TestStep>(reader)
.withType(TestStep.class)
.withIgnoreLeadingWhiteSpace(true)
.withSeparator(',')
.build();
Iterator<TestStep> csvIterator = csvToBean.iterator();
while(csvIterator.hasNext()) {
TestStep testStep = csvIterator.next();
runner.run(testStep);
}
}
}
通过实现BeforeTestExecutionCallback 接口中的beforeTestExecution方法,可以将传入的用例文件内容(测试步骤)进行解析,并交给一个Runner进行执行。
再来看一下Runner接口
package com.demo.junit5.runner;
import com.demo.junit5.bean.TestStep;
public interface Runner {
public void run(TestStep testStep);
}
其中的run接口用于具体的执行。作为示例,这里先给一个MockRunner
package com.demo.junit5.runner;
import com.alibaba.fastjson.JSON;
import static org.assertj.core.api.Assertions.assertThat;
import com.demo.junit5.bean.TestStep;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MockRunner implements Runner {
public void run(TestStep testStep) {
log.info("mock runner called");
log.info("step #{}",testStep.getNum());
log.info(JSON.toJSONString(testStep));
ssertThat(testStep.getResponse()).isEqualToIgnoringCase(testStep.getResponse());
}
}
实际工作中可以使用Rest-Assured等工具来实现HTTP接口的调用,并进行结果的验证。如果是TCP等类型的接口,换一种具体实现即可。 有经验的读者可能已经想到了,这就是一个典型的工厂设计模式的使用场景。我们用一个简单工厂作为示例:
package com.demo.junit5.runner;
public class RunnerFactory {
public static Runner getRunner(String runner) {
return new MockRunner();
}
}
目前这个工厂只提供MockRunner一种实现。
测试步骤的Bean 如下:
package com.demo.junit5.bean;
import com.opencsv.bean.CsvBindByName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TestStep {
@CsvBindByName(column="num")
private int num;
@CsvBindByName(column="type")
private String type;
@CsvBindByName(column="url")
private String url;
@CsvBindByName(column="params")
private String params;
@CsvBindByName(column="response")
private String response;
}
通过Lombok极大地简化了代码。而通过opencsv,可以极为方便地实现csv文件和bean之间地转换。
CsvToBean<TestStep> csvToBean = new CsvToBeanBuilder<TestStep>(reader)
.withType(TestStep.class)
.withIgnoreLeadingWhiteSpace(true)
.withSeparator(',')
.build();
只要通过opencsv5.0提供的建造者方法一行代码就能完成了。
至此,一个简单的自定义文件的测试框架就构建完毕了,从测试用例来看,测试方法体可以是ZeroCode,基本实现了全部测试用例在文件中体现的目标。总结一下使用到的技术点:
0.使用csv文件来定义测试用例及步骤 1.使用自定义测试注解来定义测试用例(参考ZeroCode) 2.使用Junit5提供的extension机制来实现测试执行 3.使用简单工厂类提供执行驱动 4.使用OpenCsv来实现解析 5.使用Lombok来定义Java Bean
至于参数化构建,我们将在后续完成。