🔧 函数开发指南
概述
函数是 Ryze 框架中用于生成动态数据的重要组件。通过实现 Function
接口,您可以创建自定义函数来满足特定的测试需求,如生成随机数据、处理字符串、执行计算等。
函数接口
基本接口定义
java
public interface Function {
/**
* 获取函数的唯一标识符
* @return 函数标识符,用于在表达式中调用
*/
String key();
/**
* 执行函数逻辑
* @param context 上下文对象
* @param args 参数列表
* @return 函数执行结果
*/
Object execute(ContextWrapper context, Args args);
/**
* 检查函数参数数量是否符合要求
* @param args 参数列表
* @param minCnt 最小参数数量
* @param maxCnt 最大参数数量
*/
default void checkMethodArgCount(Args args, int minCnt, int maxCnt) {
if (args.size() < minCnt || args.size() > maxCnt) {
throw new RuntimeException(
String.format("函数 %s 参数数量错误, 期望 %d ~ %d, 实际 %d",
key(), minCnt, maxCnt, args.size())
);
}
}
}
参数处理工具
java
public class Args {
// 获取字符串参数
public String getString(int index);
public String getFirstString();
// 获取数值参数
public int getIntValue(int index);
public double getDoubleValue(int index);
public boolean getBooleanValue(int index);
// 获取对象参数
public Object getFirst();
public Object get(int index);
// 参数数量和检查
public int size();
public boolean isEmpty();
}
函数开发实例
1. 基础函数示例
随机字符串函数
java
/**
* 生成指定长度的随机字符串
* 使用方式:${custom_random(10)} 或 ${custom_random(8, "abc123", true)}
*/
public class CustomRandomFunction implements Function {
@Override
public String key() {
return "custom_random";
}
@Override
public Object execute(ContextWrapper context, Args args) {
// 参数验证:0-3个参数
checkMethodArgCount(args, 0, 3);
// 获取参数,设置默认值
int length = args.size() > 0 ? args.getIntValue(0) : 8;
String chars = args.size() > 1 ? args.getString(1) : null;
boolean uppercase = args.size() > 2 ? args.getBooleanValue(2) : false;
// 生成随机字符串
String result;
if (StringUtils.isBlank(chars)) {
result = RandomStringUtils.secure().nextAlphabetic(length);
} else {
result = RandomStringUtils.secure().next(length, chars);
}
return uppercase ? result.toUpperCase() : result;
}
}
数学计算函数
java
/**
* 数学计算函数
* 使用方式:${math("add", 10, 20)} 或 ${math("multiply", 5, 3)}
*/
public class MathFunction implements Function {
@Override
public String key() {
return "math";
}
@Override
public Object execute(ContextWrapper context, Args args) {
checkMethodArgCount(args, 3, 3);
String operation = args.getString(0);
double num1 = args.getDoubleValue(1);
double num2 = args.getDoubleValue(2);
return switch (operation.toLowerCase()) {
case "add", "+" -> num1 + num2;
case "subtract", "-" -> num1 - num2;
case "multiply", "*" -> num1 * num2;
case "divide", "/" -> {
if (num2 == 0) {
throw new IllegalArgumentException("除数不能为零");
}
yield num1 / num2;
}
case "power", "pow" -> Math.pow(num1, num2);
case "mod", "%" -> num1 % num2;
default -> throw new IllegalArgumentException("不支持的运算: " + operation);
};
}
}
2. 高级函数示例
数据库查询函数
java
/**
* 数据库查询函数
* 使用方式:${db_query("SELECT name FROM users WHERE id=?", "123")}
*/
public class DatabaseQueryFunction implements Function {
private final Map<String, DataSource> dataSources = new ConcurrentHashMap<>();
@Override
public String key() {
return "db_query";
}
@Override
public Object execute(ContextWrapper context, Args args) {
checkMethodArgCount(args, 1, 10);
String sql = args.getString(0);
Object[] params = args.stream().skip(1).toArray();
try {
// 从上下文获取数据源名称,默认使用 "default"
String dataSourceName = context.getLocalVariable("datasource", "default");
DataSource dataSource = getDataSource(dataSourceName, context);
return executeQuery(dataSource, sql, params);
} catch (Exception e) {
throw new RuntimeException("数据库查询失败: " + e.getMessage(), e);
}
}
private DataSource getDataSource(String name, ContextWrapper context) {
return dataSources.computeIfAbsent(name, key -> {
// 从上下文配置中创建数据源
String url = context.getGlobalVariable(key + ".url");
String username = context.getGlobalVariable(key + ".username");
String password = context.getGlobalVariable(key + ".password");
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(username);
config.setPassword(password);
config.setMaximumPoolSize(5);
config.setConnectionTimeout(30000);
return new HikariDataSource(config);
});
}
private Object executeQuery(DataSource dataSource, String sql, Object[] params) throws SQLException {
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// 设置参数
for (int i = 0; i < params.length; i++) {
stmt.setObject(i + 1, params[i]);
}
// 执行查询
try (ResultSet rs = stmt.executeQuery()) {
List<Map<String, Object>> results = new ArrayList<>();
ResultSetMetaData metaData = rs.getMetaData();
int columnCount = metaData.getColumnCount();
while (rs.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
String columnName = metaData.getColumnLabel(i);
Object value = rs.getObject(i);
row.put(columnName, value);
}
results.add(row);
}
// 如果只有一行一列,直接返回值
if (results.size() == 1 && results.get(0).size() == 1) {
return results.get(0).values().iterator().next();
}
// 如果只有一行,返回 Map
if (results.size() == 1) {
return results.get(0);
}
// 否则返回 List
return results;
}
}
}
}
HTTP 请求函数
java
/**
* HTTP 请求函数
* 使用方式:${http_get("https://api.example.com/users/123")}
*/
public class HttpRequestFunction implements Function {
private final OkHttpClient httpClient;
public HttpRequestFunction() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build();
}
@Override
public String key() {
return "http_get";
}
@Override
public Object execute(ContextWrapper context, Args args) {
checkMethodArgCount(args, 1, 2);
String url = args.getString(0);
String returnType = args.size() > 1 ? args.getString(1) : "body";
try {
Request request = new Request.Builder()
.url(url)
.get()
.build();
try (Response response = httpClient.newCall(request).execute()) {
return switch (returnType.toLowerCase()) {
case "status", "code" -> response.code();
case "body", "content" -> {
ResponseBody body = response.body();
yield body != null ? body.string() : null;
}
case "json" -> {
ResponseBody body = response.body();
if (body != null) {
String content = body.string();
yield JSON.parseObject(content);
}
yield null;
}
case "headers" -> {
Map<String, String> headers = new HashMap<>();
response.headers().forEach(pair ->
headers.put(pair.getFirst(), pair.getSecond())
);
yield headers;
}
default -> throw new IllegalArgumentException("不支持的返回类型: " + returnType);
};
}
} catch (Exception e) {
throw new RuntimeException("HTTP 请求失败: " + e.getMessage(), e);
}
}
}
3. 工具类函数
文件操作函数
java
/**
* 文件读取函数
* 使用方式:${file_read("data/test.txt")} 或 ${file_read("data/test.json", "json")}
*/
public class FileReadFunction implements Function {
@Override
public String key() {
return "file_read";
}
@Override
public Object execute(ContextWrapper context, Args args) {
checkMethodArgCount(args, 1, 2);
String filePath = args.getString(0);
String format = args.size() > 1 ? args.getString(1) : "text";
try {
// 支持相对路径和绝对路径
Path path = Paths.get(filePath);
if (!path.isAbsolute()) {
// 相对于项目根目录
String projectRoot = System.getProperty("user.dir");
path = Paths.get(projectRoot, filePath);
}
if (!Files.exists(path)) {
throw new FileNotFoundException("文件不存在: " + path);
}
String content = Files.readString(path, StandardCharsets.UTF_8);
return switch (format.toLowerCase()) {
case "text", "string" -> content;
case "json" -> JSON.parseObject(content);
case "yaml", "yml" -> {
Yaml yaml = new Yaml();
yield yaml.load(content);
}
case "lines" -> Arrays.asList(content.split("\\n"));
case "csv" -> parseCsv(content);
default -> throw new IllegalArgumentException("不支持的格式: " + format);
};
} catch (Exception e) {
throw new RuntimeException("文件读取失败: " + e.getMessage(), e);
}
}
private List<Map<String, String>> parseCsv(String content) {
String[] lines = content.split("\\n");
if (lines.length < 2) {
return Collections.emptyList();
}
String[] headers = lines[0].split(",");
List<Map<String, String>> results = new ArrayList<>();
for (int i = 1; i < lines.length; i++) {
String[] values = lines[i].split(",");
Map<String, String> row = new LinkedHashMap<>();
for (int j = 0; j < Math.min(headers.length, values.length); j++) {
row.put(headers[j].trim(), values[j].trim());
}
results.add(row);
}
return results;
}
}
环境变量函数
java
/**
* 环境变量获取函数
* 使用方式:${env("JAVA_HOME")} 或 ${env("API_KEY", "default_key")}
*/
public class EnvironmentFunction implements Function {
@Override
public String key() {
return "env";
}
@Override
public Object execute(ContextWrapper context, Args args) {
checkMethodArgCount(args, 1, 2);
String varName = args.getString(0);
String defaultValue = args.size() > 1 ? args.getString(1) : null;
// 优先从系统环境变量获取
String value = System.getenv(varName);
// 如果环境变量不存在,尝试从系统属性获取
if (value == null) {
value = System.getProperty(varName);
}
// 如果都没有,使用默认值
if (value == null) {
value = defaultValue;
}
return value;
}
}
函数注册
SPI 注册
创建文件 src/main/resources/META-INF/services/io.github.xiaomisum.ryze.function.Function
:
com.example.CustomRandomFunction
com.example.MathFunction
com.example.DatabaseQueryFunction
com.example.HttpRequestFunction
com.example.FileReadFunction
com.example.EnvironmentFunction
动态注册
java
public class FunctionRegistry {
public static void registerCustomFunctions() {
// 在运行时动态注册函数
ApplicationConfig.getFunctions().addAll(Arrays.asList(
new CustomRandomFunction(),
new MathFunction(),
new DatabaseQueryFunction(),
new HttpRequestFunction(),
new FileReadFunction(),
new EnvironmentFunction()
));
}
}
函数使用
在测试配置中使用
yaml
title: 用户注册测试
testclass: http
config:
method: POST
url: "https://api.example.com/users"
body:
name: "${faker(\"name.fullName\")}"
email: "${custom_random(8)}@test.com"
age: "${math(\"add\", 20, 10)}"
id: "${db_query(\"SELECT MAX(id) + 1 FROM users\")}"
config_value: "${env(\"API_CONFIG\", \"default_config\")}"
在 Java 代码中使用
java
@Test
@RyzeTest
public void testWithFunctions() {
MagicBox.http("函数测试", http -> {
http.config(config -> config
.method("POST")
.url("${env(\"API_BASE_URL\")}/users")
.body(body -> {
body.put("name", "${faker(\"name.fullName\")}");
body.put("email", "${custom_random(10)}@example.com");
body.put("score", "${math(\"multiply\", 85, 1.2)}");
body.put("data", "${file_read(\"testdata/user.json\", \"json\")}");
})
);
});
}
开发规范
1. 命名规范
- 函数名使用小写字母和下划线
- 避免与内置函数冲突
- 名称应具有描述性
2. 参数处理
- 始终验证参数数量和类型
- 提供合理的默认值
- 处理参数转换异常
3. 异常处理
- 使用
RuntimeException
包装异常 - 提供清晰的错误信息
- 包含必要的上下文信息
4. 性能考虑
- 避免在函数中执行耗时操作
- 合理使用缓存机制
- 注意资源管理和释放
5. 线程安全
- 函数实例可能被多线程调用
- 避免使用实例变量存储状态
- 使用线程安全的工具类
测试函数
单元测试示例
java
class CustomFunctionTest {
private CustomRandomFunction function;
private ContextWrapper context;
@BeforeMethod
void setUp() {
function = new CustomRandomFunction();
context = mock(ContextWrapper.class);
}
@Test
void shouldGenerateRandomString() {
// Given
Args args = new Args(Arrays.asList("10"));
// When
String result = (String) function.execute(context, args);
// Then
assertThat(result).hasSize(10);
assertThat(result).matches("[a-zA-Z]+");
}
@Test
void shouldUseDefaultLength() {
// Given
Args args = new Args(Collections.emptyList());
// When
String result = (String) function.execute(context, args);
// Then
assertThat(result).hasSize(8);
}
@Test
void shouldThrowExceptionForInvalidParams() {
// Given
Args args = new Args(Arrays.asList("10", "chars", "upper", "extra"));
// When & Then
Assert.assertThrows(RuntimeException.class, () -> {
function.execute(context, args);
});
}
}
最佳实践
- 功能单一:每个函数只负责一个特定功能
- 参数清晰:提供清晰的参数文档和示例
- 错误处理:完善的异常处理和错误提示
- 性能优化:避免重复计算和资源浪费
- 兼容性:保持向后兼容性
- 文档完整:提供详细的使用文档
- 测试覆盖:编写完整的单元测试
通过遵循这些指导原则,您可以开发出高质量、可靠且易用的自定义函数。