| name | accordant-cross-language |
| description | Testing systems not written in .NET - trace-based conformance testing, JSON test plan export, and executing from any language. Use when the user's system is in Python, Go, Java, Rust, C++, or any non-.NET language, or when they ask about trace validation or exporting test cases. |
Testing Any System with Accordant
The spec is an oracle — it doesn't need to call your system, just see what happened. This skill covers how to use Accordant when the system under test isn't in .NET.
Decision Guide
| Situation | Approach |
|---|
| System has an HTTP/gRPC API | Use Accordant normally — bind operations to a .NET client. Server language doesn't matter. |
| Want to test from another language | Export test plans to JSON, execute from your runner, capture traces, validate with spec.Allows() |
| Calling native code (C/C++/Rust) from .NET | Use P/Invoke for shared libraries, COM interop, or C++/CLI |
The Two Core Capabilities
Everything else (built-in test runner, polling, request derivation) is a small convenience layer. The real value:
- Spec as oracle — given any trace (requests + responses), the spec tells you whether every response was correct given the state history
- State-graph-based test generation — the spec simulates the system, explores reachable states, and algorithms extract high-coverage test sequences
Both work from any language via trace validation and JSON export.
Trace-Based Conformance Testing
The Pattern
- System runs (any language) and produces a trace: operation name + request + response per step
- A small .NET validator walks the trace through
spec.Allows()
- The spec checks each response against expected behavior given current state
Trace Format
The spec doesn't prescribe a format. A simple JSON array works:
[
{ "operation": "CreateAccount", "request": { "AccountId": "alice" }, "response": { "StatusCode": 201, "Balance": 0 } },
{ "operation": "Deposit", "request": { "AccountId": "alice", "Amount": 100 }, "response": { "StatusCode": 200, "Balance": 100 } }
]
Requirements:
- Operation name must match a spec operation exactly
- Request/response JSON must deserialize into the C# types
- Trace starts from a known initial state
Sequential Validation
public record TraceEntry(
[property: JsonPropertyName("operation")] string Operation,
[property: JsonPropertyName("request")] JsonElement Request,
[property: JsonPropertyName("response")] JsonElement Response);
var trace = JsonSerializer.Deserialize<List<TraceEntry>>(traceJson);
var stateProfile = new StateProfile(new InitialState());
var allValid = true;
for (int i = 0; i < trace.Count; i++)
{
var entry = trace[i];
var operation = spec.GetOperation(entry.Operation);
var request = JsonSerializer.Deserialize(entry.Request.GetRawText(), operation.RequestType);
var response = JsonSerializer.Deserialize(entry.Response.GetRawText(), operation.ResponseType);
var (isValid, message, nextProfile) = spec.Allows(operation, request, response, stateProfile);
if (!isValid)
{
Console.WriteLine($"VIOLATION at step {i} ({entry.Operation}): {message}");
allValid = false;
break;
}
stateProfile = nextProfile;
}
Concurrent Validation
For overlapping operations, use spec.AllowsConcurrent(). Format trace as array of segments — single-element = sequential, multi-element = concurrent:
foreach (var segment in trace)
{
if (segment.Count == 1)
{
var (isValid, message, next) = spec.Allows(op, req, resp, stateProfile);
stateProfile = next;
}
else
{
var (concValid, concMessage, nextProfile) = spec.AllowsConcurrent(stateProfile, concurrentCalls);
stateProfile = nextProfile;
}
}
Exporting Test Plans
Generate and Save
var testCases = spec.GenerateTests(new InitialState(), inputs,
new TestGenerationOptions
{
MaxDepth = 4,
StateConstraint = s =>
});
var context = spec.CreateTestingContext();
TestCaseGenerator.SaveSequentialTestCases(context, "test-cases.json", testCases);
TestCaseGenerator.SaveConcurrentTestCases(context, "concurrent-test-cases.json", concurrentTestCases);
Exported JSON Shape
[
{
"Description": "Create alice → Deposit 100",
"OperationCalls": [
{
"Name": "Create alice",
"Input": {
"OperationName": "CreateAccount",
"Name": "Create alice",
"SerializedRequest": "{\"AccountId\":\"alice\"}"
}
}
]
}
]
Key fields: OperationName = what to call, SerializedRequest = the JSON payload. Concurrent test cases use Segments instead of OperationCalls.
Executing from Any Language (Python example)
import json
with open("test-cases.json") as f:
test_cases = json.load(f)
for test_case in test_cases:
reset_system()
trace = []
for call in test_case["OperationCalls"]:
op_name = call["Input"]["OperationName"]
request = json.loads(call["Input"]["SerializedRequest"])
response = execute_operation(op_name, request)
trace.append({"operation": op_name, "request": request, "response": response})
Polling and Derivations
These are minor convenience features — straightforward to replicate:
- Polling: Check
Input.Polling field — loop calling the poll operation with wait time between retries
- Derivations: If
Input.DerivedFromOperationCalls is set, build the request from prior responses instead of using SerializedRequest
Event-Based Systems
For systems that emit events (not request/response), model each event as a request with an empty response:
public record OrderShippedEvent(string OrderId, string TrackingNumber);
public record Unit;
spec.Operation<OrderShippedEvent, Unit>("OrderShipped", (evt, state) => { ... });
Trace entry: { "operation": "OrderShipped", "request": { ... }, "response": {} }
Key Points to Communicate
- The spec doesn't run the system — it validates what happened
- No Execute bindings needed for trace validation — only Apply logic matters
- Test generation is optional — trace validation works with any source of sequences
- The built-in test runner, polling, and derivation are small conveniences, not core features
- Always include explanation strings in
Expect.That() — they appear in violation messages
Documentation
See Testing Any System for the full guide with examples.