# genies_test
Testing infrastructure and utilities for the Genies framework. Provides generic tools for Java/Rust API comparison testing, database snapshot diffing, and assertion helpers.
## Overview
genies_test provides reusable testing building blocks for cross-language service migration validation:
- **HTTP Client**: Pre-configured HTTP client with auth support
- **Database Utilities**: Snapshot, diff, and restore for verifying database side effects
- **Deep Diff**: Recursive JSON comparison with dynamic field filtering
- **Assertions**: Strict and tolerant diff assertion helpers
- **Mutation Testing**: End-to-end mutation comparison workflow with database verification
## Features
- **Java/Rust Comparison**: Side-by-side API response comparison between Java and Rust services
- **Database Snapshot & Diff**: Capture table state before/after operations and compute changes
- **Dynamic Field Filtering**: Automatically ignore non-deterministic fields (IDs, timestamps)
- **Global Mutation Lock**: Serialize mutation tests to prevent database conflicts
- **Environment Variable Override**: All configuration values can be overridden via environment variables
- **Zero Framework Dependency**: Does not depend on other Genies internal crates
## Module Reference
### config — HTTP Client Factory
Provides a pre-configured HTTP client with optional Bearer authentication.
| `http_client()` | Create proxy-disabled HTTP client with optional Bearer auth via `TOKEN` env var |
### db — Database Test Utilities
Snapshot-based diff/restore workflow for verifying database side effects.
| `db_snapshot()` | Capture a snapshot of specified tables |
| `db_diff()` | Compute differences between two snapshots |
| `db_restore()` | Restore database state from a diff |
| `ChangeType` | Enum: `Insert`, `Delete`, `Update` |
| `DbChange` | Struct representing a single row change |
| `AffectedTable` | Struct describing a table to snapshot (name, PK, order, where clause) |
### diff — Deep Diff Utilities
Recursive JSON value comparison and field filtering.
```rust
use genies_test::diff::{deep_diff, filter_fields, filter_dynamic_diffs};
// Recursively compare two JSON values
let diffs = deep_diff("root", &json_a, &json_b);
// Remove specific fields from a JSON object
let filtered = filter_fields(&json_obj, &["id", "createTime"]);
// Filter out diffs caused by dynamic fields (id, timestamps, etc.)
let significant = filter_dynamic_diffs(&diffs);
```
### assertions — Assertion Helpers
Convenient wrappers for diff-based test assertions.
```rust
use genies_test::assertions::{assert_no_diffs, assert_no_significant_diffs};
// Strict: fail if any differences exist
assert_no_diffs("test_name", &diffs);
// Tolerant: fail only if non-dynamic differences exist
assert_no_significant_diffs("test_name", &diffs);
```
### mutation — Mutation Test Workflow
End-to-end comparison workflow for write operations with database verification.
```rust
use genies_test::mutation::{
DB_MUTATION_LOCK, compare_db_changes, test_mutation_with_db_diff,
};
// Global mutex lock ensures mutation tests run serially
let _lock = DB_MUTATION_LOCK.lock().await;
// Compare two sets of DbChange vectors
let diffs = compare_db_changes(&java_changes, &rust_changes, &dynamic_fields);
// Full 8-step mutation comparison test
test_mutation_with_db_diff(
&client, &rb, "operation_name",
&java_url, &rust_url,
Some(&request_body),
&affected_tables,
&dynamic_fields,
).await;
```
## Environment Variables
| `TOKEN` | Bearer authentication token | *(none)* |
> **Note**: Business-specific configuration (service URLs, database URLs, test IDs) should be defined in your project's test module. See the sickbed example for reference.
## Quick Start
### 1. Add Dependency
Use `cargo add` to add dependencies (automatically fetches the latest version):
```sh
cargo add genies_test
```
You can also manually add dependencies in `Cargo.toml`. Visit [crates.io](https://crates.io) for the latest versions.
### 2. Query Comparison Test
```rust
use genies_test::*;
use serde_json::Value;
// Define your project-specific URLs
fn java_base_url() -> String {
std::env::var("JAVA_BASE_URL").unwrap_or_else(|_| "http://localhost:8080/api".to_string())
}
fn rust_base_url() -> String {
std::env::var("RUST_BASE_URL").unwrap_or_else(|_| "http://localhost:8081/api".to_string())
}
#[tokio::test]
async fn compare_query() {
let client = http_client();
let java_resp: Value = client
.get(format!("{}/list", java_base_url()))
.send().await.unwrap()
.json().await.unwrap();
let rust_resp: Value = client
.get(format!("{}/list", rust_base_url()))
.send().await.unwrap()
.json().await.unwrap();
let diffs = deep_diff("api/list", &java_resp, &rust_resp);
assert_no_diffs("api/list", &diffs);
}
```
### 3. Mutation Comparison Test with Database Diff
```rust
use genies_test::*;
#[tokio::test]
async fn compare_mutation() {
let client = http_client();
// Initialize your own RBatis connection
let rb = rbatis::RBatis::new();
rb.init(rbdc_mysql::MysqlDriver {}, "mysql://user:pass@localhost:3306/mydb").unwrap();
let body = serde_json::json!({ "name": "test" });
test_mutation_with_db_diff(
&client, &rb, "create_entity",
"http://localhost:8080/api/create",
"http://localhost:8081/api/create",
Some(&body),
&[AffectedTable {
table: "my_table",
pk_field: "id",
order_by: "id",
where_clause: "name = 'test'".to_string(),
}],
&["id", "createTime", "updateTime"],
).await;
}
```
### 4. Tolerant Assertion with Dynamic Field Filtering
```rust
use genies_test::*;
#[tokio::test]
async fn compare_with_tolerance() {
let client = http_client();
let java_resp: Value = client
.get("http://localhost:8080/api/detail")
.send().await.unwrap()
.json().await.unwrap();
let rust_resp: Value = client
.get("http://localhost:8081/api/detail")
.send().await.unwrap()
.json().await.unwrap();
let diffs = deep_diff("api/detail", &java_resp, &rust_resp);
assert_no_significant_diffs("api/detail", &diffs);
}
```
## The 8-Step Mutation Test Flow
`test_mutation_with_db_diff` executes the following steps:
1. **Snapshot** — Capture initial database state for affected tables
2. **Java Call** — Send the mutation request to the Java service
3. **Java Diff** — Snapshot again and compute database changes from Java
4. **Restore** — Roll back database to the initial state
5. **Snapshot** — Capture a fresh baseline
6. **Rust Call** — Send the same mutation request to the Rust service
7. **Rust Diff** — Snapshot again and compute database changes from Rust
8. **Compare** — Assert that Java and Rust produced equivalent database changes
## Dependencies
- **once_cell** — Lazy static initialization
- **rbatis** — Database access
- **reqwest** — HTTP client
- **serde_json** — JSON serialization and comparison
- **tokio** — Async runtime
- **rbs** — RBatis serialization helpers
## Integration Guide
### Migration Notes
1. **genies_test provides generic tools only** — Business-specific configuration (service URLs, database connections, test IDs, cleanup functions) must be defined in each project's `tests/common/mod.rs`.
2. **RBatis initialization is the caller's responsibility** — `db_snapshot`/`db_diff`/`db_restore` require a `&RBatis` instance passed by the caller.
3. **Sort JSON arrays before comparison** — To avoid false positives caused by inconsistent array ordering, sort arrays by a key field (e.g. `sort_json_arrays`) before calling `deep_diff`.
4. **Unified re-export pattern** — Use `pub use genies_test::*;` in `tests/common/mod.rs` to re-export all generic tools, then add project-specific functions in the same file.
### Project Integration Example
```toml
# Cargo.toml
[dev-dependencies]
genies_test = { path = "../../crates/test" }
tokio = { version = "1", features = ["full"] }
serde_json = "1"
rbatis = "4"
rbdc-mysql = "4"
```
```rust
// tests/common/mod.rs
pub use genies_test::*;
use rbatis::RBatis;
use rbdc_mysql::MysqlDriver;
// Business-specific configuration
pub fn java_base_url() -> String {
std::env::var("JAVA_BASE_URL").unwrap_or_else(|_| "http://localhost:8080/api".into())
}
pub fn rust_base_url() -> String {
std::env::var("RUST_BASE_URL").unwrap_or_else(|_| "http://localhost:8081/api".into())
}
pub fn database_url() -> String {
std::env::var("TEST_DATABASE_URL").unwrap_or_else(|_| "mysql://user:pass@host/db".into())
}
pub async fn init_test_rbatis() -> RBatis {
let rb = RBatis::new();
rb.init(MysqlDriver {}, &database_url()).unwrap();
rb
}
/// Sort JSON arrays by key to avoid ordering mismatches
pub fn sort_json_arrays(value: &mut serde_json::Value, sort_key: &str) {
match value {
serde_json::Value::Array(arr) => {
arr.sort_by(|a, b| {
let ak = a.get(sort_key).and_then(|v| v.as_str()).unwrap_or("");
let bk = b.get(sort_key).and_then(|v| v.as_str()).unwrap_or("");
ak.cmp(bk)
});
}
serde_json::Value::Object(map) => {
for (_, v) in map.iter_mut() { sort_json_arrays(v, sort_key); }
}
_ => {}
}
}
```
## Integration with Other Crates
genies_test is designed as a standalone testing utility. It does not depend on any other Genies internal crates and can be used independently in any project that requires Java/Rust API comparison testing.
## License
See the project root for license information.