背景
自动化测试框架中需要支持计算解析断言表达式
需求
发送完一个http请求后,需要对响应的内容做精确校验,实现方式:填写断言表达式,框架实现对类似如下表达式的计算解析。
( [$.statusCode] == 200 OR [$.comments] == '成功' ) AND ${v_cardNo} != '12345' AND ${v_money} > 100.98 AND ${v_money} > 0.98
- [json路径],中括号中间的表示http请求响应的参数路径
- ${变量名},美元符和大括号中间的表示用例执行上下文中储存的变量
- 字符串用单引号引用
- 支持等于、不等于、大于、大于等于、小于、小于等于、与、或等操作
实现
1. 新建maven工程,并创建一个g4文件
src/main/resources 目录下,命名:Expression.g4
内容如下
// 定义了 grammar 的名字,名字需要与文件名对应
grammar Expression;
@header {
// 定义package
package com.bobo.learn.antlr.core;
}
// Lexer rules
WS: [ \t\r\n]+ -> skip;
STRING: '\'' (~['\r\n])* '\'';
NUMBER: ([0-9][0-9]*)+(.[0-9]+)?;
EXTRACT_PATH: '[' ~[[\]]+ ']';
VARIABLE_NAME: '${' ~[${}]+ '}';
LPAREN: '(';
RPAREN: ')';
EQUAL: '==';
NOT_EQUAL: '!=';
LESS_THAN: '<';
LESS_EUQAL: '<=';
LARGER_THAN: '>';
LARGER_EQUAL: '>=';
AND: 'AND';
OR: 'OR';
// Parser rules
// #后面的名字,是在后续处理对应规则的时候会用到的
expression
: LPAREN expression RPAREN #expressionWithBr
| EXTRACT_PATH op=(EQUAL | NOT_EQUAL) STRING #expressionWithString
| EXTRACT_PATH op=(EQUAL | NOT_EQUAL) VARIABLE_NAME #expressionWithString
| EXTRACT_PATH op=(EQUAL | NOT_EQUAL) EXTRACT_PATH #expressionWithString
| EXTRACT_PATH op=(EQUAL | NOT_EQUAL | LESS_THAN | LESS_EUQAL | LARGER_THAN | LARGER_EQUAL) NUMBER #expressionWithNumber
| VARIABLE_NAME op=(EQUAL | NOT_EQUAL) STRING #expressionWithString
| VARIABLE_NAME op=(EQUAL | NOT_EQUAL) EXTRACT_PATH #expressionWithString
| VARIABLE_NAME op=(EQUAL | NOT_EQUAL) VARIABLE_NAME #expressionWithString
| VARIABLE_NAME op=(EQUAL | NOT_EQUAL | LESS_THAN | LESS_EUQAL | LARGER_THAN | LARGER_EQUAL) NUMBER #expressionWithNumber
| expression AND expression #andOperation
| expression OR expression #orOperation
;
2. 安装antlr4插件
此插件可以调试,antlr4语法规则
3. 调试语法规则
- 在语法上右键-》点击test rule expression
- 输入表达式,可以到右下方会出现生成的解析树,如果有错误,会有提示
4. 生成java源码文件
- 右键g4文件,点击Configure ANTLR
- 选择生成的源码文件的目标目录
在这里直接与g4文件配置的package目录对应
- 右键g4文件,点击Generate ANTLR
- 生成结果
5. 测试
- 新建测试代码ExpressionTest.java
package com.bobo.learn.antlr;
import com.bobo.learn.antlr.core.ExpressionBaseVisitor;
import com.bobo.learn.antlr.core.ExpressionLexer;
import com.bobo.learn.antlr.core.ExpressionParser;
import cn.hutool.core.util.ReUtil;
import lombok.extern.slf4j.Slf4j;
import org.antlr.v4.runtime.CharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.apache.commons.lang3.StringUtils;
@Slf4j
public class ExpressionTest {
public static void main(String[] args) {
final String expr = "( [$.statusCode] == 200 OR [$.comments] == '成功' OR [$.comments] == ${v_hello} ) AND ${v_cardNo} != '12345' AND ${v_money} > 100.99";
log.info(expr);
CharStream input = CharStreams.fromString(expr);
ExpressionLexer lexer = new ExpressionLexer(input);
CommonTokenStream tokens = new CommonTokenStream(lexer);
ExpressionParser parser = new ExpressionParser(tokens);
ExpressionParser.ExpressionContext context = parser.expression();
AssertExpressionVisitor visitor = new AssertExpressionVisitor();
Object result = visitor.visit(context);
log.info("Expression result: {}", result);
}
static class AssertExpressionVisitor extends ExpressionBaseVisitor<Object> {
@Override
public Object visitExpressionWithBr(ExpressionParser.ExpressionWithBrContext ctx) {
return visit(ctx.getChild(1));
}
@Override
public Object visitExpressionWithString(ExpressionParser.ExpressionWithStringContext ctx) {
String left = ctx.getChild(0).getText();
String right = ctx.getChild(2).getText();
if(ctx.op != null) {
log.info("原始:{} {} {}", left, ctx.op.getText(), right);
String leftOperand = null;
String rightOperand = null;
if(left.startsWith("[")) {
leftOperand = extract(left) + "";
}else if(left.startsWith("${")) {
leftOperand = getVariableValue(left) + "";
}
if(right.startsWith("[")) {
rightOperand = extract(right) + "";
}else if(right.startsWith("${")) {
rightOperand = getVariableValue(right) + "";
}else if(ctx.STRING() != null) {
rightOperand = ctx.STRING().getText();
rightOperand = rightOperand.substring(1, rightOperand.length() - 1);
}
log.info("实际:{} {} {}", leftOperand, ctx.op.getText(), rightOperand);
switch (ctx.op.getType()) {
case ExpressionLexer.EQUAL:
return StringUtils.equals(leftOperand, rightOperand);
case ExpressionLexer.NOT_EQUAL:
return !StringUtils.equals(leftOperand, rightOperand);
default: throw new RuntimeException("unsupported operator type");
}
}
return super.visitExpressionWithString(ctx);
}
@Override
public Object visitExpressionWithNumber(ExpressionParser.ExpressionWithNumberContext ctx) {
String left = ctx.getChild(0).getText();
String right = ctx.getChild(2).getText();
if(ctx.op != null) {
log.info("原始:{} {} {}", left, ctx.op.getText(), right);
Double leftOperand = null;
Double rightOperand = Double.valueOf(right);
Object leftString = null;
if(left.startsWith("[")) {
leftString = extract(left);
} else if(left.startsWith("${")) {
leftString = getVariableValue(left);
}
if(leftString != null && ReUtil.isMatch("([1-9][0-9]*)+(.[0-9]+)?", leftString.toString())) {
leftOperand = Double.valueOf(leftString.toString());
}
log.info("{} {} {}", leftOperand, ctx.op.getText(), rightOperand);
if(leftOperand == null) {
return false;
}
switch (ctx.op.getType()) {
case ExpressionLexer.EQUAL:
return rightOperand.equals(leftOperand);
case ExpressionLexer.NOT_EQUAL:
return !rightOperand.equals(leftOperand);
case ExpressionLexer.LESS_THAN:
return leftOperand < rightOperand;
case ExpressionLexer.LESS_EUQAL:
return leftOperand <= rightOperand;
case ExpressionLexer.LARGER_THAN:
return leftOperand > rightOperand;
case ExpressionLexer.LARGER_EQUAL:
return leftOperand >= rightOperand;
default: throw new RuntimeException("unsupported operator type");
}
}
return super.visitExpressionWithNumber(ctx);
}
@Override
public Object visitOrOperation(ExpressionParser.OrOperationContext ctx) {
Object left = visit(ctx.getChild(0));
Object right = visit(ctx.getChild(2));
log.info("实际:{} {} {}", left, ctx.OR().getText(), right);
if(left instanceof Boolean && right instanceof Boolean) {
return (Boolean)left || (Boolean)right;
}
return false;
}
@Override
public Object visitAndOperation(ExpressionParser.AndOperationContext ctx) {
Object left = visit(ctx.getChild(0));
Object right = visit(ctx.getChild(2));
log.info("{} {} {}", left, ctx.AND().getText(), right);
if(left instanceof Boolean && right instanceof Boolean) {
return (Boolean)left && (Boolean)right;
}
return false;
}
/**
* 根据路径提取http响应参数的值
* @param path
* @return
*/
public Object extract(String path) {
if("[$.statusCode]".equalsIgnoreCase(path)) {
return 200;
}
if("[$.comments]".equalsIgnoreCase(path)) {
return "成功";
}
return "";
}
/**
* 提取用例执行上下文中保存的变量的值
* @param variableName
* @return
*/
public Object getVariableValue(String variableName) {
return 100;
}
}
}
- 执行测试代码,可以看到一个解析过程,测试结果如下
maven工程pom文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.bobo</groupId>
<artifactId>learn</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.18</version>
</dependency>
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4-runtime</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
</project>