mojito
mojito是一个简单而灵活的测试框架,最初开发它是为了解决OpenSearch前端测试中遇到的一些问题,但是随着mojito的开发,我们发现它可以被应用在更广泛的范围里。
mojito目前只是测试工具,而非测试服务。用户可以按照其规则,通过yaml文件定义测试工程和测试用例,并使用mojito来执行这些用例。而mojito的一个强大之处在于,它可以很方便的进行扩展。在mojito看来,任何测试用例都是由一组“测试行为(action)”组成的,除了先天内置的一些action外,用户可以直接在yaml文件里自定义新的action,来基于已有action进行扩展或组合。后面会有详细介绍。
在自定义action的基础上,mojito还支持优美的上下文系统。用户可以分别在工程(project)、用例(case)、行为(action)这几个方面进行递进的变量定义和传递。
在公司内部,有一些和mojito类似的测试框架,例如tiramisu和ATest,mojito不是为了替代这些框架,而是纯粹为了更好的适应OpenSearch的测试。已有框架在扩展性和易用性上存在一些问题,使得OpenSearch开发自己的mojito的成本在某种意义上低 于应用那些框架。同时,mojito也是搜索测试对Clojure语言应用在测试开发中的一个尝试,不得不说,到目前为止,收效符合预期。
名称来源:mojito原意是一种鸡尾酒 (link),来源于古巴,是大作家海明威的最爱。话说起这个名字的由来也属偶然。mojito的开发过程参考了很多tiramisu的思想和“外观”,于是在起名字的时候,就也想找一个甜点蛋糕的名字来命名,结果没找到什么合适的。此时刚好想起了夏天最爱喝的一种酒精饮料:mojito。青柠的酸爽配上薄荷的清凉,白朗姆的甘冽沉浸在苏打水的泡泡中,简直是夏日的绝佳消暑饮品。帝都找不到正宗的mojito卖,于是每年夏天,老婆都会给我自制上几次,即使是冬天,那味道也常常在脑海中回荡。给这个工具起这个名字,也是希望我们的测试功能能够爽酷起来吧。
安装
请从此处下载mojito的最新版本
下载后解压,并把解压后的文件夹添加到可执行目录即可。
使用方法
注意:请在使用前确保jre1.7已经被正确安装,且java命令在当前path路径内
运行项目:
mojito run -p "project-path"
获取帮助:
mojito -h
使用概述
如前文所说,mojito的核心理念是:所有的用例都由一系列“行为”组成,在mojito里被称为action。而一个测试项目就是多个测试用例(case)的组合,case之间是对等且隔离的,它们可以共用一些全局项目配置,但是在执行阶段是相互独立的。从 项目 到 用例 再到 行为,mojito是通过上下文系统来使之串联起来。
了解mojito的用法,最好的方式是通过示例工程。每个发布包里都包含一个sample-project目录,里面的yaml文件都包含大量注释或示例以供参考。
目录结构
先来看一下mojito的项目目录结构。打开发布包里的sample-project目录,你会看到如下的结构:
SAMPLE-PROJECT
│ project.yml
│
├─actions
│ call-openapi.yml
│ create-index.yml
│ delete-index.yml
│
├─cases
│ case1.yml
│ case2.yml
│ mysql-case.yml
│
├─data
│ mysql.csv
│ test_main.txt
│
├─mail
│ report.html
│
└─templates
各个目录(文件)的含义如下:
- project.yml文件(必选):项目的核心定义文件,定义了项目的全局信息(如 项目名称、项目描述等)和配置(如 并行数),也定义了顶层的环境变量(参考 上下文系统)
- cases目录(必选):所有的用例定义都必须在此目录下,可以有多层的目录
- actions目录(可选):存放用户的自定义action。在 自定义action 一节会有更详细的描述
- data目录(可选):存放用户用例中使用到的数据文件。其实mojito并没有规定用户数据文件放到哪儿,只是鼓励用户按照此规则存放
- mail目录(可选):用于存放用户的邮件模板。(此目录名只是约定,用户可以选用任何目录名)
- templates目录(保留):未来的mojito还会支持用例模板的功能,所以此目录保留给该功能使用,目前用户只需保留其为空即可
注意:所有mojito文件必须使用“UTF-8无BOM编码”,否则会出现乱码或其他未知错误
Hello Mojito
既然是程序员,第一个程序一定是Hello World。不过此处我们是 Hello Mojito。按照如下步骤,来创建我们的第一个测试项目,开启mojito之旅吧!
- 首先,我们创建一个测试项目的根目录,此处我们放在 d:\hello-mojito 下面
-
在该目录下创建一个文本文件,project.yml,其内容如下(注意:使用“UTF-8无BOM编码”):
name: hello-mojito desc: this is a HelloWorld project for mojito envs: - name: default desc: default environment config set-vars: strings: string1: Hello string2: Mojito
-
在该目录下创建一个新目录,命名为 cases
-
在cases目录下创建一个文本文件,hello-mojito.yml,其内容如下:
id: 1 title: Hello Mojito desc: Mojito上的第一个Hello World用例 envs: [default] set-vars: k: ${strings.string1}$ ${strings.string2}$ site: http://m.sm.cn steps: - action: http-action invoke-params: method: GET url: ${site}$/s headers: User-Agent: "Mozilla/5.0 (Windows NT 6.1; WOW64)" params: q: ${fn:urlescape(k)}$ by: submit from: smor ensure: equals: status: 200
-
然后执行指令
mojito run -p "d:\hello-mojito"
如果一切顺利,你会得到如下的输出:
2015-04-28 18:10:04 - [INFO] start executing project [hello-mojito]
2015-04-28 18:10:04 - [INFO] project params: verbose=0, env=default, tags=[], priority=[3], caselist=[], send-email=[true]
2015-04-28 18:10:04 - [INFO] thread 0 get a case [hello.yml], start to run
2015-04-28 18:10:04 - [INFO] start executing of case [hello.yml], title: Hello Mojito
2015-04-28 18:10:04 - [INFO] enter case startup
2015-04-28 18:10:04 - [INFO] leave case startup, result: success
2015-04-28 18:10:04 - [INFO] enter case steps: null
2015-04-28 18:10:04 - [INFO] start executing of action [http-action]
2015-04-28 18:10:05 - [INFO] action [http-action] successed
2015-04-28 18:10:05 - [INFO] end executing of action [http-action]
2015-04-28 18:10:05 - [INFO] leave case steps, result: success
2015-04-28 18:10:05 - [INFO] enter case teardown
2015-04-28 18:10:05 - [INFO] leave case teardown, result: success
2015-04-28 18:10:05 - [INFO] case [hello.yml] final result: PASS
2015-04-28 18:10:05 - [INFO] end executing of case [hello.yml]
2015-04-28 18:10:05 - [INFO] thread 0 finish case [hello.yml] running
2015-04-28 18:10:05 - [INFO] ================================================================================
2015-04-28 18:10:05 - [INFO] Project [hello-mojito] results
2015-04-28 18:10:05 - [INFO] ================================================================================
2015-04-28 18:10:05 - [INFO] Summaries:
2015-04-28 18:10:05 - [INFO] Start: 2015-04-28T10:10:04.754Z End: 2015-04-28T10:10:05.062Z
2015-04-28 18:10:05 - [INFO] Total: 1 Pass: 1 Fail: 0 Skip: 0 Timeout: 0
2015-04-28 18:10:05 - [INFO] Pass Rate: 100.00% Pure Pass Rate: 100.00%
2015-04-28 18:10:05 - [INFO] --------------------------------------------------------------------------------
2015-04-28 18:10:05 - [INFO] Case deatils:
2015-04-28 18:10:05 - [INFO] 2015-04-28T10:10:05.061Z PASS hello.yml
2015-04-28 18:10:05 - [INFO] ================================================================================
一切都很简单自然。我们一共执行了1个case,执行结果是PASS。其中该case(hello.yml)共有3个阶段,分别为startup、steps和teardown。我们在case的定义文件中,只定义了steps,没有定义startup和teardown,所以那两个步骤没有任何action需要执行。在steps阶段,只有一个action,就是http-action。如果在执行的时候使用-v参数,可以得到更详细的log,能够看到action的输入和输出。
再稍微多解释一下case的定义。在hello.yml的steps下面是一个列表,列出了需要执行的每个action,这些action会被顺次执行。此例中我们只有一个http-action调用。在action调用里面,我们传入了两组关键参数,其一是invoke-params,另一个是ensure。
- invoke-params:其内容为一个map,包含了action执行所需的参数。参数可以是任何类型(string、number、list、map),但其解释权归action所有。此例中的http-action,我们传了4个关键参数,用于调用神马搜索。
- ensure:定义了对action执行结果的验证,其值为一个map,其中map的key为验证类型。目前mojito只支持3种基本验证类型(equals、matches、exists),未来会支持更多的验证类型,以及验证的逻辑关系(and、or、not)。mojito认为,任何action的执行,其返回值都是一个map、list的组合。可以通过特定的语法来访问action的返回值,从而进行验证。例如在此例中,我们验证返回值的“status”字段(http-action的返回值是一个map)必须等于200,否则就会报错。更多关于ensure的内容参加下文 “action执行结果检验(ensure)”
定义project
在前文已经看到,每个测试工程都会有一个project.yml文件,用于定义工程整体的一些设置。其格式如下:
---
name: sample-project
desc: this is a sample project for opstest
team: OpenSearch
owner: [xu.zhao]
thread: 1
envs:
- name: test
desc: test environment config
set-vars:
taobao-domain: sandbox.taobao.com
search-domain: sandbox.search.taobao.com
- name: online
set-vars:
taobao-domain: www.taobao.com
search-domain: s.taobao.com
...
其中各个参数的含义如下:
- name: 必选,项目的名称。此配置不影响运行,但此参数会在结果统计中用到。建议使用英文名称,并且不使用空格
- desc: 可选,工程的描述,可随意填写,不影响运行。主要是用于给出工程的更详细描述(如被测对象是什么等)
- team: 可选,项目所属工作组,可随意填写,不影响运行。但建议填写有意义的名称。
- owner: 可选,列表形式,任务的所有者(建议填写)。请确保此处填写公司的alias,从而方便与其他系统对接或数据统计
- thread: 可选,任务运行时使用的并行线程数,默认为 1。mojito支持多线程执行测试用例。在mojito的概念里,case与case之间是完全对等的,且是相互无关的,所以可以随意并行执行。所以case会被放到哪个线程执行是完全无法预期的。即使只有1个线程,case间的先后执行顺序也是不确定的,用户在使用mojito时,不能假设case的执行顺序。如果case间使用了公共资源,且需要互斥访问,可以使用lock方法(后文会有介绍)
- envs: 必选,为工程定义若干个环境设置。在运行时,可以指定case运行在哪个环境上。请至少指定1个env,否则用例将无法正确运行。测试过程中我们经常会遇到这种情况,你设计了某个case,该case的逻辑非常清晰,且可以运行在多个不同环境上(测试、集成、预发、生产等)。case在不同环境上运行时,测试逻辑完全相同,但配置参数是不同的。mojito的每个env定义,都可以独立定义一组“变量”,在case运行时,会根据所运行的环境不同,而使用不同的env变量定义,从而把一个case“变”成多个环境上的多个case。换句话说,case定义了测试逻辑“模板”,而运行时通过env里的变量设置,来把case模板实例化。
- name: 必选,env环境的名称(唯一标识)。请使用英文,无空格
- desc: 可选,此env的附加描述,不影响运行,但建议填写有意义的说明
- set-vars: 必选,map格式,此env所对应的根变量设置。其内容随意填写,在“上下文系统”一节里会有更详细的使用描述
定义case
在定义好project之后,就可以定义case了。mojito要求所有case必须定义在cases目录下,每个case是一个独立的yaml文件,其文件名(包含相对cases的路径)构成了该case的唯一标识。
用户可以用目录来组织不同的case,但在mojito看来,这些case除了“唯一标识”不同外,没有区别。换句话说,mojito里没有case group的概念。但把case用目录管理起来除了“美观”外,还有一个额外的好处,就是在执行阶段通过-c参数来指定程序路径。通过-c指定的路径是一个正则表达式,所以可以通过这种方式“按目录筛选”case来执行。
case的yaml文件定义如下:
---
id: <id>
title: <case title>
desc: <case description>
envs: [test, pre]
set-vars:
var1: [string1, string2]
var2: 2
var3:
var3-1: [311, str312]
var3-2:
var3-2-1: xxx
var3-2-2: yyy
url: http://${search-domain}$/s
lock: <lock key>
startup:
- action: sleep
lock: <another lock key>
invoke-params:
second: 2
steps:
- action: http-action
invoke-params:
method: GET
url: ${url}$
params:
q: ${fn:urlescape(k)}$
ensure:
equals:
status: 200
teardown:
- action: some-action
invoke-params:
xxx: 1
yyy: b
...
其中各个参数的含义如下:
- id: 必填,数字或字符串,代表用例的id,由用户自己指定,没有特殊含义,也不影响运行(即使不同的case指定了相同的id也没关系)。但建议此处指定唯一的id,而且最好和kelude上的文本case的id一致(如果有文本case的话)。mojito未来会做成一个服务,届时此id将由系统自动生成。
- title: 必填,字符串,代表程序的名称,由用户自己指定,没有特殊含义,也不影响运行。建议使用可读的简要描述,不宜过长。通常此字段会出现在最终的report中。
- desc: 选填,字符串,对此用例的相对详细的描述,由用户自己指定,没有特殊含义,也不影响运行。
- envs: 必填,列表,用于标识此case可以运行在哪些环境上。在mojito的执行阶段,会通过命令行指定一个env,如果该env不在case的envs列表里,则该case不会被执行
- set-vars: 选填,map格式,用于在case级别定义一组vars。此定义与project.yml里的set-vars一致,只不过在case范围内生效。如果和project里定义了同名的var,则case里的定义会覆盖project里的。这里定义var的时候,可以通过 ${xxx}$ 来引用上下文系统中已经定义的变量,如此例中,我们引用了project里定义的 search-domain 来定义一个新的var,名称为 url。关于 上下文系统 后文还会有详述。
- lock: 选填,字符串,用于在多线程并行时,进行case间的互斥锁定(类似于 enter-lock)。相同lock key的case不会并行执行。lock可以放在case级别,也可以放在action级别,取决于锁何时锁定和释放。请注意,mojito没有专门处理死锁的机制,需要用户自己来避免。lock名称的定义同样可以引用上下文系统。
- startup、steps、teardown: 选填,action列表。mojito认为,任何case都是由一组actions组成,startup会被最先执行,然后是steps,最后是teardown。如果startup失败,则steps不会被执行。但teardown始终会被执行,且teardown不可以依赖startup或steps的执行。
-
action格式: action的定义是一个map,分如下字段:
- action: action的名称(该名称必须是系统已经存在的,内置或自定义均可,关于自定义action后文有详述)。如果action名称不存在,则会报错
- lock: 与case的lock定义相同,不过只锁定该action的执行阶段
- set-vars: 定义一组只在action执行阶段有效的vars。
- invoke-params: 必选,map格式,传递给action执行的参数。其解析由具体的action负责。
-
output-transfer: 可选,用于定义如何对action的输出结果进行转义。举个例子,可能某个http调用的返回值是json(string),但是我们如果直接使用该string进行结果校验,就必须书写大量复杂的正则表达式。那么可以在output-transfer阶段,对结果进行调整,把body里的内容进行json解析,并生成结构化数据,并存储在json-body字段中,那么后续的验证就可以基于此而完成,如下:
output-transfer: - parse-http-json-body
-
ensure: 可选,map格式,定义如何验证此action的执行结果。如果验证失败,则此action执行失败。具体的结果校验规则,详见 “action执行结果检验(ensure)”
-
export-vars: 可选,map格式,与set-vars类似,其作用是,在action的output里选择“某些”内容,然后输出到上下文系统中。export-vars定义的所有vars,在其后的action执行中是可见的
使用详解
通过上面的Hello Mojito,相信你已经有了初步的使用概念,下面详细展开一些使用细节。
命令行详解
Mojito的命令行模式和hadoop很像,基本是如下的格式:
mojito <command> [parameters...]
目前,command支持如下类型:
- mojito run: 运行测试用例,其支持参数如下
- -p project-path: 可选,用于指定待执行的测试工程所在的路径,也就是project.yml所在的路径。默认值为当前路径。
- -e environment-name: 可选,用于指定所执行的env(前文对env已有介绍)。env是在project.yml里面配置的,这里所指定的名称,必须和配置中的一致。如果不指定,则使用project.yml里配置的第一个env。
- -c case-path-regex: 可选,可指定多个-c参数,通过正则表达式对case执行路径进行筛选。case执行路径是相对于 /cases 的相对路径。不指定时默认执行所有case。如果指定了多个-c,按“或”关系对case进行筛选。
- -v: 可选,用于指定程序的verbose level。当添加此参数时,程序会打印更多细节log
- -t tag: 可选,可指定多个-t参数,通过所指定的tag对case进行筛选。默认执行所有case。当指定多个-t时,case的tag中任何一个匹配到任何一个-t的tag都会得到执行
- -n exclude-tag: 可选,可指定多个-n。此参数与-t相反,如果匹配,则不执行该case。此参数与-t共同作用时,优先级高于-t
- -P priority: 可选,可指定多个-P。根据case的优先级定义进行筛选。
- -m TRUE/FALSE: 可选,用于enable/disable结果邮件的发送。默认不发送结果邮件
- mojito help: 打印帮助信息
命令行示例:
# 执行当前目录的test project
mojito run
# 执行d:\sample-project下的case,在bts环境下执行,用标签【OpenAPI】进行过滤,只执行P1和P2的case
mojito run -p d:\sample-project -e bts -t OpenAPI -P1 -P2
自定义 action
mojito的基本理念就是:case就是action的组合,依次调用action就执行了case。前文我们以http-action为例,讲解了action的基本调用。但细心的话会发现,现有的方式使用起来还是很不方便的。如果我们需要反复调用一个相似的action怎么办?比如在OpenSearch中,所有的API请求都需要进行一个鉴权验证,需要根据http-method、query-paramters、key、secret等信息生成一个token。这个动作会反复出现,难道我们需要每次填写一大堆参数么?Mojito为此提供了一个解决方案,就是 自定义action。
所谓 自定义action,就是从已有的一个或多个action出发,定义一个新的action。类似于面向对象里的类集成或组合。所有的自定义action都需要放到特定的位置:/actions。该目录下的所有yaml文件都会被当做自定义action来解析,使用者可以自主划分多级目录进行组织,每个自定义action一个单独的yaml文件。目前mojito支持3种action扩展,分别是:extend、combine和repeat。
EXTEND-ACTION
extend-action是最常用的自定义action,一般用于简化action的重复调用。其定义格式如下:
---
name: my-extend-action
type: extend-action
base: http-action
required-params: [param1, param2]
set-vars:
local-var1: ${param1.x}$
local-var2: ${param2}$
invoke-params:
url: http://xxx.com/api/v2
headers:
X-API-Header: MyAction
method: GET
params:
some-param: ${local-var1}$
another-param: ${local-var2}$
output-transfer:
- parse-http-json-body
ensure:
equals:
status: 200
json-body.status: OK
...
上面的例子示例了extend-action的基本定义方法。my-extend-action从http-action“派生”而来,case里面可以直接调用my-extend-action,但真正执行阶段会去执行http-action。my-extend-action相当于对http-action的调用参数进行了“预设”,或者说是一个调用模板。
开头的四个参数是基本信息:
- name: 此action的名称,在case里面调用action时,填写此名称。如果定义了同名的自定义action则只有一个定义会被保留,且无法确定保留哪个。目前mojito不做重名检查,需要用户自己保证名称的唯一性。
- type: 此处填写extend-action,表示对已有的单一action直接进行包装扩展
- base: 被“派生”的action。执行阶段真正执行到的就是这个action。可以是mojito内置的action,也可以是其他的用户扩展action。mojito在执行阶段才会检查base action的合法性,且目前mojito没有做循环依赖检查,所以用户需要自己保证自定义action之间不会有循环依赖
- required-params: 必选参数名称列表。当case里调用此action时,mojito会对调用参数做出检查,如果必选参数没有填写,则直接报错。
后面的信息格式与case里调用action的格式是一样的,唯一的区别就是,用户传进来的参数在上下文中也是可用的,在此例中是 param1 和 param2。
COMBINE-ACTION
combine-action是另一种扩展action的方式,其目的是简化多个action的组合调用,把它们抽象成一个公共的action。例如,在OpenSearch测试中,很多case都需要先尝试删除某个表,然后再重新创建该表,则可以把这两个操作组合定义成delete-and-create-table操作,从而简化case中的书写。
---
name: my-combine-action
type: combine-action
required-params: [param1, param2]
set-vars:
local-var1: ${param1.x}$
local-var2: ${param2}$
steps:
- action: inner-action-1
invoke-params:
p1: ${local-var1}$
p2: some other info
- action: inner-action-2
set-vars:
y: ${param1.y}$
invoke-params:
p1: xyz${local-var2}$
p2: hello ${y}$
output-transfer:
- parse-http-json-body
ensure:
equals:
json-body.status: OK
...
在这个例子中,我们把两个已有的action(不一定是内置action,有可能是其他自定义action)进行了组合,从而组成了新的my-combine-action,当调用该action时,实际上是依次调用了inner-action1和inner-action-2。前面的几个参数没有什么特别的,而后面steps部分也和case里面的steps格式一样,每个inner action都可以有单独的set-vars、output-transfer和ensure。
整个combine action会最终以最后一个执行到的inner action的output作为整体output。此例中,如果两个inner action都执行成功,则会以inner-action-2的output作为最终output(经过output-transfer之后)。
REPEAT-ACTION
TODO...
optional-params
- optional-params:action可选参数, 配置默认key-value。当case里调用此action提供默认key的值,使用case action的传入值,未提供时使用定义的默认值 比如,下面默认参数opt-keys1 opt-key2
yaml---name: create-indextype: extend-actionbase: call-openapirequired-params: [host, user, index-name, template, template-type]optional-params:opt-key1: 10opt-key2: 20...
### 上下文系统
Mojito的魅力之一就是拥有一套良好的上下文系统,以及功能强大的表达式系统。在前例中我们已经看到,在set-vars中,我们经常看到 ${xxx.yyy}$ 这样的写法,其实就是在引用上下文中的变量。
变量的定义
Mojito对上下文变量的处理类似于编程语言函数调用里面的局部变量。最外层到最内层分别是:
env -> case -> action -> inner action -> inner inner action ...
内层调用可以访问外层的变量,反之则不可以,同名变量内层会覆盖外层。变量定义是直接通过yaml里的map实现的,例如:
set-vars:
a: 1
b: mojito
上面的代码,分别定义了两个变量,a的值是数字1,b的值是字符串“mojito”。而变量的值可以是“复杂”结构,比如list或map:
set-vars:
c:
- 1
- 2
- 3
d:
m: v1
n: v2
需要注意的是,虽然可以定义复杂结构深层次的变量,但变量定义的首层必须是一个map。
变量的引用
Mojito支持在多个地方对定义的变量进行引用,例如在 set-vars 中通过 ${param-name}$ 来访问。所有的数据都是以 map、list、primitive 的某种组合存在的(类似json或yaml所能表述的内容),为了更好的引用变量,Mojito支持通过表达式来进行复杂的访问。假设我们在env中有如下数据:
set-vars:
a: 1
b: [str1, str2, str3]
c:
m: abc
d:
n:
- x: 1
y: y1
- x: 2
y: y2
- x: 3
y: y3
- 通过key访问map的value
- a: 直接访问变量a,会得到1
- c.m: 多层key通过“.”连接,用于访问内层变量,此例中会得到字符串“abc”
- 通过索引访问列表元素
- b.[1]:通过下标索引(从0开始)访问列表子元素,此例中会取到“str2”
- b.[add(-1, 2)]:区别于之前,这里的下标索引不在是特定值,而是来源于函数 add 的调用结果。
- 调用函数
- add(5, d.n.[0].x):此处调用了函数add,传入两个参数,第一个是 5,第二个取自d变量第一个元素的x的值,所以,此表达式会得到 6。通过这个例子我们可以看到,函数的参数仍旧可以传一个表达式。
- 特殊的key
- <ROOT>:任何时候,通过都可以访问到“根”变量,或者说整个变量表。Mojito为了简化函数调用时候的书写,当以表达式作为函数的参数时,内层表达式的求值会以外层当前访问到的值作为基础。
- <CUR>:在case的ensure部分,默认访问到的是当前action的output,所有基于key或索引的访问都会从action output出发(这么设计是为了简化书写)。所以在ensure里面, <ROOT> 会访问到整个变量表,而 <CUR> 则会访问到当前action output的根
- <ITE> 和 <IDX>:这两个特殊的key是用来对list进行筛选时使用的,下面有详细的讲解
在上面的例子中,我们看到Mojito的表达式支持函数调用,此处有必要介绍几个特殊函数
- filter函数,用于对list进行过滤
- 格式为 filter(base-path, cond1, cond2 ...),其中base-path对当前数据进行访问,预期取到的是一个list,否则返回null
- 例子
- filter(d.n, gt(x, 1)) 的含义为:过滤list,选出所有x元素大于1的那些。此表达式会得到一个list,包含两个元素{x:2, y:y2}和{x:3, y:y3}。
- filter(d.n, gt(x,1)).[0].y 选出的元素可以继续通过key和索引来访问,此表达式会得到字符串“y2”。
- 如果函数调用时填写多个condition,则条件间相互为“与”的关系
- <ITE> 和 <IDX> 的使用
- filter(d.n, gt(x,1)) 等价于 filter(d.n, gt(<ITE>.x, 1)),ITE为iterator的缩写
- filter(d.n, gt(<IDX>, 1)) 的含义为:如果list的某个元素“下标”大于1,则将其筛选出来
- every函数 与filter函数格式类似,不过返回值为True/False。如果list的所有元素都满足条件,则返回Ture,否则返回False
- debug函数,用于打印更多的帮助信息
- 格式: debug(inner-expression)
- 作用: 对内部的表达式进行求值,将求值信息打印在log中,同时返回求值结果。debug函数不对数据产生影响,只是为了打log
- default函数,如果求值失败,则使用默认值
- 格式: default(inner-experssion, default-value)
- 示例: default(xxx.yyy, "hello") 因为xxx.yyy不存在,所以默认值“hello”生效
action结果格式转化(output-transfer)
action的输出直接使用可能会比较“难用”,所以mojito支持对结果进行转化,其方式就是对action的output依次执行一系列的transfer,如下:
- action: http-action
invoke-params:
url: http://xxx.net/api
params:
p1: 1
output-transfer:
- parse-http-json-body
- type: parse-json
from: x.0.y
to: another-json-fields
- type: simple-select-member
select:
id: ${json-body.id}$
info: ${another-json-fields.info}$
ensure:
equals:
id: 1
output-transfer有两种写法,如果没有参数,则直接填写transfer的名称即可,如例子中的 parse-http-json-body。如果有参数,则以map的形式给出,其中type是必选的,填写transfer的名称,其他内容会由具体的transfer来规定。后文介绍 内置output-transfer 时候会给出详细信息
action执行结果检验(ensure)
之前的示例里已经看到,每个action调用都可以通过ensure部分来检查action的返回值是否正确。ensure部分是一个map,其格式如下:
ensure:
equals:
x: expact1
m.0.n: 3
matches:
y: hello\s+W.*
advance:
- or(gt(a.b, 1), eq(x, "expact1"))
- fun1(x.y, fun2(aaa), 3)
目前支持4种方式对结果进行校验,下面分别介绍
- equals方式:输入为一个map,其中,key用于对结果进行访问,而值用于表示预期
- 注意,此处的key虽然也能够像前文介绍的那样通过表达式来访问结果值,但是只支持按照key和index来访问,如此例中的 m.0.n 表示访问结果的key "m" 的 第0个元素 的key "n"。
- matches方式:与equals类似,不过是做正则匹配
- exists方式:输入为一个列表,每一项表示对结果的某个字段进行访问,如果得到的值不为空,则通过验证
- advance方式:输入为一个列表,每行为一个“全功能”的表达式(可以用上下文表达式的任何功能和函数)。如果表达式求值不为空或False,则通过验证。
测试结果展现
目前Mojito在两个地方展现测试结果:邮件和日志。
结果邮件
如果要enable结果邮件,则需要在project.yml里进行正确的配置,下面以注释的方式进行阐述:
# send-email-config: 可选,用于发送结果邮件的smtp陪着。如果不填,则不发送
send-email-config:
host: smtp.alibaba-inc.com
port: 465
ssl: yes
user: xu.zhao@alibaba-inc.com
pass: "password"
# email-to: 选填,结果邮件的发送目标,如果有多个,则用yaml列表方式表
# 示。不填写代表不发送结果邮件
email-to: [xu.zhao@alibaba-inc.com]
# email-from: 建议填写,结果邮件发送时的发送人。如果不填写,则使用
# mojito@list.alibaba-inc.com。但是这有可能造成smtp服务器端验证失败而
# 拒绝发送邮件
email-from: xu.zhao@alibaba-inc.com
# email-template: 建议填写,结果邮件的渲染模板。如果不填写,则使用默
# 认模板,那是一个很难看的模板哦~~~
email-template: mail/report.html
envs:
-
name: test
desc: test environment config
# 下面的3个配置可以覆盖全局的send-email-config里面的配置。如果全局配
# 置了邮件发送,但在此env下想disable掉,可以将 email-to 设为空 list,
# 如下: email-to: []
email-to: [xu.zhao@alibaba-inc.com, other@list.alibaba-inc.com]
email-from: xu.zhao@alibaba-inc.com
email-template: mail/report.html
在这个配置里,我们指定了一个邮件渲染的模板,请参考这里来书写你自己的模板(我那个实在太难看了)。该模板使用了一个叫做selmer的模板语言,具体可以参考:这里。也可以参考OpenSearch测试用到的email模板,http://svn.simba.taobao.com/svn/QA/Scripts/Etao/EtaoOpenSearch/opensearch-api-test/mail/report.html
日志
TODO...
内置功能
内置 action 列表
目前mojito已经支持一系列的内置action,每个功能不同,主要体现在调用参数和结果结构的区别。下面详细介绍。
http-action
目前用的最多的action,用于发起http请求,其invoke-params如下:
invoke-params:
# 必选,要访问的url(可以不带query string)
url: http://xxx.net/api
# 可选,http请求的method,默认为GET
method: POST
# 可选,map形式给出query string。例如下面的例子会最终修改url为
# http://xxx.net/api?q=hello&uid=12345
query-params:
q: hello
uid: 12345
# 可选,以map形式给出PUT、POST请求的form参数
form-params:
name: xu.zhao
email: xu.zhao@alibaba-inc.com
# 可选,以map形式给出参数,如果请求是put、post,则会合并到form-params里,
# 如果是get、head类请求,则会合并到query-params里
params:
p1: 1
p2: pppp
# 可选,直接给出put、post请求的http body。如果不填写body-encoding,则默认使用UTF-8
body: information for post
body-encoding: UTF-8
# 可选,http请求的header,以map形式给出
headers:
User-Agent: Mozilla/5.0 (Windows NT 6.1;) Gecko/20100101 Firefox/13.0.1
foo: [bar, baz]
# 可选,不验证SSL server证书的合法性,默认为false
insecure?: true
http请求是包含cookie的,每个case在启动时会有一个空的cookie,但整个case运行过程中cookie不会被清空,且前面的action的cookie会影响后面的请求。
http-action的结果是个一map,有如下主要字段:
- body: 返回的body(string格式)
- headers: response里的headers
- cookies: server端返回的cookie,以map形式展现,key为cookie的name,其值仍是一个map,为cookie的具体信息,每个cookie都包括如下字段: value、domain、path、discard、secure
- throw-exceptions: 可选,http请求返回异常status值是否抛异常,默认true。
mysql-query
执行sql查询,并返回其结果。其输入格式如下:
invoke-params:
# 必选,指定数据库的链接信息
db-spec:
host: your.mysql.host
user: root
password: your password
name: your_db_name
# 必选,指定要执行的sql
sql: select * from foo where id > 0 order by id
其返回值是一个map,包含两个字段:
- count:返回的结果总数
- results:为一个列表,为所有查到的结果,每个结果是一个map,key为字段名,value为字段值
mysql-execute
执行一条带输入的sql,可以用于创建表或插入数据等,其输入格式如下:
invoke-params:
db-spec: 同上省略
# 必选,要执行的sql语句,其格式与jdbc中一致,变量部分用?代替
sql: insert into foo (id, name) values (?, ?)
# 可选,如果sql语句中有“?”,则此处必填,执行时会用此处填写的内容代替问好部分。
# 如果执行的建表一类的操作,则不需填写此字段。
data:
- [1, name1]
- [2, name2]
此action的返回值是一个map,只有一个字段,affected,为受影响的结果行数。
mysql-execute-with-csv
此action与mysql-execute类似,不过其输入为csv文件(而非data字段)。
invoke-params:
db-spec: 同上省略
sql: insert into foo (id, name) values (?, ?)
file: data/sqldata.csv
sleep
这是一个没啥用的action,只是使执行线程sleep指定的秒数
invoke-params:
second: 30
call-opensearch-api
这是一个专为opensearch的v3版本api调用开发的action
invoke-params:
# 必选,opensearch的验证信息
access-key-id: your key id
access-key-secret: your secret
# 必选,请求的url
url: http://xxxx.net/api
# 可选,默认为get
method: get/put/post/....
# 可选,内容任意,会被转换成json字符串,然后放到http请求的body里
body-params:
something: xxxxx
# 可选,直接给定json字符串内容,作为请求的body。此字段优先级高于body-params,如果指定
# 了此字段,则会忽略body-params的内容
body-json: "{\"a\": 1}"
# 可选,http请求的headers(与http-action中一致)
headers:
x-opensearch-foo: bar
其返回值与http-action的返回值格式一致,不过会增加一个额外的字段:json-body,为response的body部分进行json parse的结果。而body里保存的则是原始的body输出。
sh-cmd
这是一个执行shell命令的action
invoke-params:
# 必选,要执行的shell命令
cmd: your shell command line
# 必选,shell运行目录, 可以使用project目录的相对路径或者绝对路径(配置为空时使用project目录)
dir: shell execute dictionary
# 必选,shell运行需要额外指定的环境变量(不需要时可配为空)
env:
env_key: xxxx
其返回值是个results的map, exit:return code, out: 标准正确输出, err: 标准错误输出
上下文系统支持的 function 列表
基本运算
- add/sub/mul/div
- gt/ge/lt/le/eq
列表运算
- first/second/last
- count
逻辑运算
- and/or/not
- if
字符串处理
- upper/lower
- format(与c语言里的printf语法相同)
时间相关
- now/local-now/time-to-long/time-from-long
- date-time:给定年月日时分秒,生成一个datetime对象,例如date-time(1980,1,1,15,30,0)
- time-plus:将指定时间加上指定偏移(单位:秒),得到新的时间。
- format-date-time:将时间格式化为字符串,如 format-date-time(now(), "YYYYMMdd")
- time-gt/time-lt:比较两个时间的大小
其他
- urlescape: 对给定字符串进行url encode
- repeat-join: 格式 repeat-join(times, str, sep),将str重复n次,并以sep连接,例如 repeat-join(3, "?", ",") 会得到 "?,?,?"
内置 output transfer 列表
- parse-http-json-body: 从结果的 body 字段按照json格式parse成结构化数据,然后存储到 json-body 字段中。
- parse-json:有两个参数,from和to。从output的 from 中取出字符串,按照json格式parse成结构化数据,并存入 to 中。from和to可以是多层的,如a.0.b。
- simple-select-member:从结果里选出特定的内容,放到新的字段中。此transfer有一个参数 select,其值为一个map,map里面的key为将要写入的字段,而value则为要写入的内容,可以使用前面提到的上下文系统里的全部语法。
其他
Mojito使用Kelude对需求和bug进行管理,项目地址为: http://k3.alibaba-inc.com/project/show?projectId=40655012
已知缺陷
计划功能
未来展望
Mojito并不能算作一个多么新颖的设计,只是把已有的各个工具中我们看中的一些功能吸纳了进来。且目前Mojito还有很多整体结构上的不完善,未来还有很多改进方向。此处谈一下目前想到的改进方向。
分布式用例执行
目前mojito是一个单一的java进程,内部采取多线程的方式来提高并行度。可以想见,在某些测试场景,可能需要多机多个“agent”来并行执行测试用例,以加快测试执行速度。
服务式用例托管和执行
mojito当前的定位是一个纯粹的测试工具,用户定义好project、action和case后调用mojito来执行。用户需要自主解决环境部署、用例维护等工作。如果mojito以服务模式运行,允许用户在mojito服务里托管工程和用例,那么也会给mojito增色不少。同时减少一些版本维护等工作。
不过换个角度,用svn或者git管理测试工程,在工程师眼里还是很自然的一件事情,所以从我个人来讲不太认为把mojito作为服务提供有多么大的必要性。
界面化用例编写
mojito完全以yaml来组织工程和用例,而用例只是简单的action的“堆砌”,所以是有可能提供一组界面来简化case、project的编写。
功能扩展
目前想为mojito添加功能扩展,如增添内置action、output-transfer等,只能使用Clojure语言直接添加代码,并且没有明确抽出扩展接口(需要对已有代码足够熟悉才能添加功能)。这部分未来可以定义更加明确的扩展接口,并且支持用其他语言(如python或java)来编写扩展。