之前在一些博客中零零散散看到过对Karate介绍,基本都和Graphql接口测试绑定在一起,似乎测试Graphql API首选的工具之一就是Karate。后来一位开发大牛也推荐我使用Karate,他提到自己之前的项目中就用框架测试Graphql接口,且强调该框架在ThoughtWorks的技术雷达中。想着Graphql使用越来越广泛,且技术雷达中介绍过的框架一般都有其独特优势,带着这些好奇心我花了一些时间来研究这个框架并对其进行了总结,如果你对Graphql接口测试或者Karate感兴趣,不妨继续阅读下面的内容。
Karate是什么
Karate是一款将接口自动化测试、mock、性能测试集合到一起的测试框架。采用BDD语法,对于无编程能力的人也很容易;另外提供强大的JSON、XML断言功能及并发执行。以上的内容翻译自Karate官网,也许你看到这些描述时仍然不能直观感受到Karate和其他接口测试框架的区别,接下来让我们看一个Karate编写的接口测试demo。以下是一个Graphql的接口,下面是利用Karate实现该接口测试的代码
利用Karate编写接口测试脚本
demo.Feature代码
Feature: test a graphql demo //feature描述信息,一个feature可以包含多个scenario
Scenario Outline: test a graphql demo //scenairo描述信息
* text queryString = //定义graphql的查询语句
"""{
product(id: <id>) {
name
description
category {
name
}
}}"""
Given url 'https://demo.getsaleor.com/graphql/' //调用接口采用given-when-then格式
And request {query : '#(queryString)'}
When method post
Then status 200 //校验接口返回200状态码
And match response.data.product.category.name == <expectValue>
//match是Karate中的关键字,常用于接口response的校验
Examples: //可以看到Karate支持data-driven
| id | expectValue |
| "UHJvZHVjdDo4Nw==" | 'Footwear' |
| "UHJvZHVjdDo3Nw==" | 'Juices'
DemoRunner代码,以下代码为固定写法,作用是运行feature中编写的内容
package examples.demo;
import com.intuit.karate.junit5.Karate;
public class DemoRunner {
@Karate.Test
Karate testDemo() {
return new Karate().feature("demo").relativeTo(getClass());
}
}
生成美观的测试报告,且能查看接口调用的Request和Response通过上面的demo可以看到正如Karate官网所介绍的那样,即便是无任何编程经验的人只要稍加学习就能编写Feature中的代码实现接口调用。另外,该工具生成的测试报告不仅美观还能查看接口调用的Request和Response,利于Debug。看到这里感觉Karate似乎确实优于其他工具,但真实项目中实现接口测试时除接口调用外,还需考虑其他内容,Karate是否真的优于其他测试工具,还得看看在这些方面是否支持良好,首先让我们看看接口测试中需要考虑的其他内容。
1.测试数据管理,即能自动准备和清理测试数据。
2.配置信息管理,便于切换到不同环境运行。
3.接口Response Schema的校验。
4.默认等待机制,保证接口测试的稳定性。因为接口调用完成后,需要对接口调用结果进行校验,可能是校验接口Response Body中的内容是否于数据库数据相等,也可能是直接查看数据库数据是否正确,而数据落入一般晚于接口调用完成,所以在很多地方需要添加默认等待机制。
5.对接口测试中用到的敏感数据进行脱敏处理等等。
第1,4,5点归纳起来是“接口测试框架与编程语言结合”,便于对数据库数据进行增删改查,便于调用编程语言包好的方法,这些方法可能是对敏感数据的脱敏处理,可能是默认等待,可能是数据库数据的二次处理等等。
接下来让我们看看Karate在“与编程语言结合”、“处理数据库数据”、“配置信息管理”以及“Response Schema校验”方面支持是否良好?
Karate调用Java方法Demo(Karate只支持Java)
名称为“Service”的Java Class,该代码中包含了两个方法
package util;
import static java.lang.Thread.sleep;
public class Service {
public void defaultWait(int i) throws InterruptedException {
sleep(1000 * i);
System.out.println("wait " + i + " seconds");
}
public String returnHello(String name) {
return "Hello " + name;
}
}
在Feature文件中调用编写好的Java方法,可以看到因为只能在Feature文件中调用,所以可读性方面有点差。
Feature: call java function demo
Scenario: call java function demo
* def service =
"""
function(arg) {
var Service = Java.type('util.Service');
var jd = new Service();
return jd.returnHello(arg);
}
"""
# in this case the solitary 'call' argument is of type string
* def result = call service 'qiaotl' //第一种调用方式
* match result == 'Hello qiaotl'
# using a static method - observe how java interop is truly seamless !
* def Service = Java.type('util.Service')
* def javaService = new Service()
* javaService.defaultWait(1) //第二种调用方式
利用Karate操作数据库数据Demo
实际Karate调用数据库有两种方式,第一种是利用Java编写好增删改查数据的方法,然后在Feature文件中调用Java方法,第二种是直接利用Karate提供的方法操作数据库数据。第一种调用Java方法的方式上面的Demo已演示,这里就演示如何利用Karate直接操作数据库数据。Feature文件中直接连接数据库查询数据Demo代码如下所示,可以看到和调用Java方法类似,在可读性方面有点差。
Feature: call database demo
Scenario: call database
# use jdbc to validate
* def config = { username: 'sa', password: '', url: 'jdbc:h2:mem:testdb', driverClassName: 'org.h2.Driver' }
* def DbUtils = Java.type('com.intuit.karate.demo.util.DbUtils')
* def db = new DbUtils(config)
# since the DbUtils returns a Java Map, it becomes normal JSON here !
# which means that you can use the full power of Karate's 'match' syntax
* def dogs = db.readRows('SELECT * FROM DOGS')
* match dogs contains { ID: '#(id)', NAME: 'Scooby' }
* def dog = db.readRow('SELECT * FROM DOGS D WHERE D.ID = ' + id)
* match dog.NAME == 'Scooby'
* def test = db.readValue('SELECT ID FROM DOGS D WHERE D.ID = ' + id)
* match test == id
利用Karate管理配置信息
在配置信息管理方面,Karate提供了原生支持,初始化项目时就会自动生成配置信息管理文件“karate-config.js”。如果有其他环境相关的配置信息,只需在此文件中添加即可,所以在配置信息管理方面Karate支持的还算比较好。
function fn() {
var env = karate.env; // get system property 'karate.env'
karate.log('karate.env system property was:', env);
if (!env) {
env = 'dev';
}
var config = {
env: env,
myVarName: 'someValue'
}
if (env == 'dev') {
// customize
// e.g. config.foo = 'bar';
} else if (env == 'e2e') {
// customize
}
return config;
}
利用Karate校验Response Schema
按官网的介绍Karate提供了一种比JSON-schema更简单且功能更强大的方式来验证接口的Response Schema,即利用Karate对Response Schema进行校验,需要先学习Schema校验中的语法规则。例如如下小例子
* def foo = ['bar', 'baz']
# 校验foo是一个数组
* match foo == '#[]'
# 校验foo是一个长度为2的数组
* match foo == '#[2]'
# 校验foo是一个长度为2的数组,且数组的值都是string类型
* match foo == '#[2] #string'
# 数组中每个element都有个length 属性,且length的值都是3
* match foo == '#[]? _.length == 3'
# 校验数组的每个值都是string且长度都等于3
* match foo == '#[] #string? _.length == 3'
如果对一个接口的Response Schema进行校验,Feature中的代码如下,可以看到相较于直接采用Json Schema的接口测试工具(例如Rest-Assured),利用Karate进行Response Schema校验需要单独学习Karate的语法,有一定的学习成本。
* def oddSchema = { price: '#string', status: '#? _ < 3', ck: '##number', name: '#regex[0-9X]' }
* def isValidTime = read('time-validator.js')
When method get
Then match response ==
"""
{
id: '#regex[0-9]+',
count: '#number',
odd: '#(oddSchema)',
data: {
countryId: '#number',
countryName: '#string',
leagueName: '##string',
status: '#number? _ >= 0',
sportName: '#string',
time: '#? isValidTime(_)'
},
odds: '#[] oddSchema'
}
"""
可以看到使用Karate,接口调用、校验的核心脚本都在Feature文件中。另外支持很多自定义关键字,例如match、contain等,以此进一步降低接口测试编写脚本成本,真正让无任何编程经验的人也能快速上手。如果只是编写“调用一个接口”这样简单的场景,Karate的简单易用以及原生支持BDD确实很不错。但对于一个复杂系统,接口测试中需要覆盖的场景不紧紧是接口调用本身,而Karate中“核心脚本都在Feature文件中”的特点恰恰让该工具出现了局限性,例如调用Java方法,连接数据库等。正所谓成也萧何败也萧何。
开篇提到ThoughtWorks的技术雷达中有推荐该框架,那具体信息如何呢?该框架确实出现在2019年上半年的技术雷达中,处于Access。技术雷达中对该框架的详细描述是“Karate是一个API测试框架,其特殊之处是直接用Gerkin编写而不依赖任何通用编程语言。Karate实际是一个描述API 测试的域语言,尽管这种方法很有趣,并且为简单测试提供了可读性很强的文档,但用于match和校验payload的特定语言可能变得语法繁重和难于理解。从长远来看以这种风格编写的复杂测试是否易用阅读和易用理解还有待观察”。可以看到技术雷达中即提到该工具的亮点同时也提到这种风格的编写对复杂测试可能不易阅读和难于理解。
除此之外,开篇我们还提到这个框架总是和Graphql接口测试绑定在一起介绍,那么该工具在Graphql接口测试中有特殊优势么?接下来让我们看看利用Karate调用Graphql接口和利用Rest-Assured(另外一款接口测试工具)调用Graphql接口的对比,使用的被测接口是第一个Demo中的接口。
Karate调用Graphql接口
demo.Feature代码
Feature: test a graphql demo //feature表述信息,一个feature可以包含多个scenario
Scenario Outline: test a graphql demo //scenairo描述信息
* text queryString = //定义graphql的查询语句
"""{
product(id: <id>) {
name
description
category {
name
}
}}"""
Given url 'https://demo.getsaleor.com/graphql/' //调用接口采用given-when-then格式
And request {query : '#(queryString)'}
When method post
Then status 200 //校验接口返回200状态码
And match response.data.product.category.name == <expectValue>
//match是Karate中的关键字,常用于接口response的校验
Examples: //可以看到Karate支持data-driven写法
| id | expectValue |
| "UHJvZHVjdDo4Nw==" | 'Footwear' |
| "UHJvZHVjdDo3Nw==" | 'Juices'
Rest-Assured调用Graphql接口
package com.github.graphqlDemo
import groovy.json.JsonSlurper
import org.junit.Assert
import spock.lang.Specification
import static io.restassured.RestAssured.given
class GraphqlTest extends Specification {
def "call graphql demo"() {
given: "get graphql query string" //采用spock框架实现BDD
def queryString = new QueryBody()
.setId(id)
.getQueryBody() //通过引入模版引擎工具参数化接口的request body
when: "call the api"
def res = given().baseUri("https://demo.getsaleor.com")
.header("Content-Type", "application/json")
.body(queryString)
.post("/graphql")
.then().statusCode(200)
.extract().response().getBody().asString()
//调用接口并获取response body
def jsonResponse = new JsonSlurper().parseText(res)
//将string类型response body转换为数据对象
then: "should return correct response"
Assert.assertEquals(jsonResponse.data.product.category.name, expectValue)
//校验response body内容
where:
id | expectValue //采用了data-driven
"UHJvZHVjdDo4Nw==" | "Footwear"
"UHJvZHVjdDo3Nw==" | "Juices"
}
}
可以看到Rest-Assured也可以实现Graphql接口测试,因为Graphql接口实际也是Http请求。那Karate是否有特殊优势呢?实际没有,例如Karate支持在请求的Request Body中传入参数,Rest-Assured虽然不原生支持,但可以借助模版引擎工具实现参数化。Karate支持BDD,Rest-Assured虽然不原生支持,但可以套用Groovy官网的BDD框架Spock实现BDD。看起来Rest-Assured使用过程中需要套用其他框架,增加了使用成本,但正是因为Rest-Assured没有集成各种其他框架让其保持了灵活性,可以和多种编程语言、其他测试框架无缝衔接。
结束语
如果在接口测试工具中一定要做一个选择,对于Java技术栈的同学来说还是强烈建议使用Rest-Assured,第一该工具2010年就推出了第一个release版本,github上的star数已超过4000+,使用非常广泛。第二工具名称虽然叫Rest-Assured,但可以利用该工具完成Graphql的接口测试。第三该工具支持和多种语言结合使用,例脚本语言Groovy。就接口测试而言最好还是采用脚本语言,因为接口测试本身没有复杂的逻辑处理,脚本语言足够了,选用Java这一类语言重了些,会增加接口测试维护成本。如果你对Rest-Assured感兴趣可以订阅Gitchat中的“接口测试实战”专栏进行学习(https://gitbook.cn/gitchat/column/5dbbe297e29af65d1d01b8fc)。