技术点:
最近笔者在尝试基于应用日志来自动生成测试用例。这其中就需要一个配套的简易测试框架。梳理了一下,其中的技术点有: 0.使用csv文件来定义测试用例及步骤 1.使用自定义测试注解来定义测试用例(参考ZeroCode) 2.使用Junit5提供的extension机制来实现测试执行 3.使用简单工厂类提供执行驱动 4.使用OpenCsv来实现解析 5.使用Lombok来定义Java Bean 6.使用Junit5提供的参数化测试解决方案junit-jupiter-params来实现测试用例集
来自ZeroCode的参考
ZeroCode是一个轻量级的开源测试框架。它通过使用JSON或者YAML文件格式来定义测试用例,进而让测试用例的编写变得更为容易。 以下是其github项目首页提供的案例
代码语言:javascript复制 @Test
@Scenario("test_customer_get_api.yml")
public void getCustomer_happyCase(){
// No code goes here. This remains empty.
}
其中test_customer_get_api.yml中就描述了这个接口测试用例的全部要素,具体如下:
代码语言:javascript复制---
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.
实际项目中的需求
在实际的测试过程中,对于文本格式的测试用例,往往有以下的需求:
- 测试用例的步骤描述通常是自定义的,而不是根据工具提供的DSL来编写。
- 用例编写尽可能少一些冗余的内容,以便节约用例编写时间。例如在前述接口测试案例中的head,Content-Type等等,在某个系统的接口规范中,往往都是规定了固定格式的。
- 可能的话,@Test之类的Java代码也不用写了。测试人员只写用例文件,框架通过扫码文件目录和文件来执行用例。
为了实现上述需求,这就要求根据测试的特点,来定制一个类似的简易测试框架。
使用文件来定义测试用例和步骤
当设计一个自动化测试用例框架时,有一个很重要的三联问问题:
如何定义一个用例?如何定义用例的步骤?如何定义一个用例集?
在本案例中,我们约定
- 一个文件(csv)是一个用例
- 文件中的一行是用例的一个步骤
- 包含若干文件的目录,组成了一个用例集
至于用csv文件来作为用例的载体,而不是json/yaml等更新的文件类型,或者xml/excel等传统文件类型,主要是基于以下两方面考虑
- 接口自动化测试,尤其是面向业务功能的测试,其请求、入参、出参的结构相对固定。
- csv擅长表达结构固定的数据内容,且格式冗余最小。
因此,如果以前述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怎么没有体现?等等问题。 这里我们假设,
- head,content-type,token这些内容在功能测试时,基本属于不变化的内容。
- status code =200,如果有业务层面的错误,在response中可以通过errorCode的方式体现。 每个框架都有其应用场合。上面这样的框架,主要是应用于业务层面的测试,而不是接口自身的鲁棒性测试等场合。这样简单的格式,也非常适合不太能写代码的同学来写自动化测试用例。
以下是编写完成以后的一个测试用例的样例
代码语言:javascript复制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中的内容就是前述表格中的内容
[未完待续]
自定义注解
我们来看下测试用例中的注解的具体实现:
代码语言:javascript复制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测试框架的功能。
具体的Extension
JUnit5提供了非常友好的扩展性,最常用的是Before/After配套的一些Callback接口上,如下图所示:
这里我们就使用了一个BeforeTestExecutionCallback的接口来进行扩展,在被注解的用例执行之前,Junit5会首先调用该接口,实现自定义的功能。
代码语言:javascript复制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相关
再来看一下Runner接口
代码语言:javascript复制package com.demo.junit5.runner;
import com.demo.junit5.bean.TestStep;
public interface Runner {
public void run(TestStep testStep);
}
其中的run接口用于具体的执行。作为示例,这里先给一个MockRunner
代码语言:javascript复制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等类型的接口,换一种具体实现即可。 有经验的读者可能已经想到了,这就是一个典型的工厂设计模式的使用场景。我们用一个简单工厂作为示例:
代码语言:javascript复制package com.demo.junit5.runner;
public class RunnerFactory {
public static Runner getRunner(String runner) {
return new MockRunner();
}
}
目前这个工厂只提供MockRunner一种实现。
业务Bean -TestStep
测试步骤的Bean 如下:
代码语言:javascript复制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之间地转换。
代码语言:javascript复制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
至于参数化构建,我们将在后续完成。