accordant-test-generation

$npx mdskill add microsoft/accordant/accordant-test-generation

Accordant generates tests by exploring the state graph defined by your spec, then running the generated sequences against your real system.

SKILL.md

.github/skills/accordant-test-generationView on GitHub ↗
---
name: accordant-test-generation
description: How Accordant generates and runs tests - use this skill when configuring test generation, understanding state graphs, or running tests
---

# Test Generation in Accordant

Accordant generates tests by exploring the state graph defined by your spec, then running the generated sequences against your real system.

## The State Graph

Starting from an initial state, Accordant applies each input to compute resulting states, then recurses. This builds a graph where:
- **Nodes** = States the system can be in
- **Edges** = Operations that transition between states

## Generating Tests

### Basic Usage

```csharp
var spec = CreateSpec();
var initialState = new BankState();

// Define inputs to explore
var inputs = new InputSet
{
    spec.GetOperation<string, ApiResult<decimal>>("CreateAccount").With("alice", "Create alice"),
    spec.GetOperation<(string, decimal), ApiResult<decimal>>("Deposit").With(("alice", 100m), "Deposit 100"),
    spec.GetOperation<(string, decimal), ApiResult<decimal>>("Withdraw").With(("alice", 30m), "Withdraw 30"),
    spec.GetOperation<(string, decimal), ApiResult<decimal>>("Withdraw").With(("alice", 200m), "Withdraw 200 (overdraft)"),
    spec.GetOperation<string, ApiResult<decimal>>("DeleteAccount").With("alice", "Delete alice"),
};

// Generate test cases
var testCases = spec.GenerateTests(initialState, inputs, new TestGenerationOptions
{
    MaxDepth = 5
});
```

### TestGenerationOptions

| Option | Description | Default |
|--------|-------------|---------|
| `MaxDepth` | Maximum sequence length | 5 |
| `StateConstraint` | Prune states during exploration | None |
| `MaxConcurrencyLevel` | For concurrent test generation | 3 |
| `SequentialTestCaseAlgorithm` | How paths are extracted | StateCoverage |

### Constraining the State Space

Prevent state explosion with `StateConstraint`:

```csharp
var options = new TestGenerationOptions
{
    MaxDepth = 5,
    StateConstraint = state =>
    {
        var s = (BankState)state;
        // Limit: max 2 accounts, balances under 500
        return s.Accounts.Count <= 2 && 
               s.Accounts.Values.All(b => b <= 500);
    }
};
```

## Test Case Algorithms

### StateCoverage (Default)

Visits each unique state at least once:

```csharp
var options = new TestGenerationOptions
{
    SequentialTestCaseAlgorithm = SequentialTestCaseAlgorithms.StateCoverage
};
```

### TransitionCoverage

Exercises every edge (transition) in the graph — can generate many tests:

```csharp
var options = new TestGenerationOptions
{
    SequentialTestCaseAlgorithm = SequentialTestCaseAlgorithms.CreateTransitionCoverage(maxSequenceLength: 4)
};
```

### RandomWalk

Samples paths probabilistically — good for large state spaces:

```csharp
var options = new TestGenerationOptions
{
    SequentialTestCaseAlgorithm = SequentialTestCaseAlgorithms.CreateRandomWalk(
        numberOfWalks: 200,
        maxWalkLength: 8,
        seed: 42)  // For reproducibility
};
```

## Running Tests

### Basic Execution

```csharp
var context = spec.CreateTestingContext();

// Register your test client
var client = new BankApiClient(httpClient);
context.Register(client);

var results = await spec.RunTests(context, initialState, testCases, new TestExecutionOptions
{
    BeforeEachAsync = async _ =>
    {
        // Reset system before each test
        await client.DeleteAccount("alice");
        await client.DeleteAccount("bob");
    }
});

// Check results
var failures = results.Where(r => !r.Success).ToList();
Assert.IsEmpty(failures, $"Failed: {failures.FirstOrDefault()?.LastFailureMessage}");
```

### TestExecutionOptions

| Option | Description |
|--------|-------------|
| `BeforeEachAsync` | Reset system before each test case |
| `AfterEachAsync` | Cleanup after each test case |
| `OnStepExecuted` | Log each operation's request/response for debugging |

## Resetting State

Each test expects the system to start in the initial state. Reset with `BeforeEachAsync`:

```csharp
var knownAccounts = new[] { "alice", "bob" };

var results = await spec.RunTests(context, initialState, testCases, new TestExecutionOptions
{
    BeforeEachAsync = async _ =>
    {
        foreach (var accountId in knownAccounts)
        {
            await client.DeleteAccount(accountId);  // Ignore 404
        }
    }
});
```

## Visualizing the State Graph

Generate a GraphViz visualization:

```csharp
var dot = spec.VisualizeStateSpace(initialState, inputs, new TestGenerationOptions { MaxDepth = 4 });
File.WriteAllText("state-graph.dot", dot);

// Convert to PNG: dot -Tpng state-graph.dot -o state-graph.png
```

### Custom Node Labels

```csharp
var dot = spec.VisualizeStateSpace(
    initialState,
    inputs,
    new TestGenerationOptions { MaxDepth = 3 },
    new VisualizationOptions
    {
        NodeLabelLambda = node =>
        {
            var state = (BankState)node.State;
            return $"Accounts: {state.Accounts.Count}\\nTotal: {state.Accounts.Values.Sum()}";
        }
    });
```

## Manual Conformance Testing

Validate individual operations without auto-generation:

```csharp
var stateProfile = new StateProfile(new BankState());

// Execute and validate
var response = await client.CreateAccount("alice");
(bool isValid, string message, stateProfile) = 
    spec.Allows(spec.GetOperation("CreateAccount"), "alice", response, stateProfile);

Assert.IsTrue(isValid, message);

// Continue with more operations...
var withdrawResponse = await client.Withdraw("alice", 50);
(isValid, message, stateProfile) = 
    spec.Allows(spec.GetOperation("Withdraw"), ("alice", 50m), withdrawResponse, stateProfile);

Assert.IsTrue(isValid, message);
```

## Complete Example

```csharp
[Test]
public async Task AutoGeneratedTests()
{
    var spec = CreateSpec();
    
    // Bind to real API
    spec.ExecuteWith<BankApiClient>()
        .Bind<string, ApiResult<decimal>>("CreateAccount", (c, id) => c.CreateAccount(id).Result)
        .Bind<(string, decimal), ApiResult<decimal>>("Deposit", (c, r) => c.Deposit(r.Item1, r.Item2).Result)
        .Bind<(string, decimal), ApiResult<decimal>>("Withdraw", (c, r) => c.Withdraw(r.Item1, r.Item2).Result)
        .Bind<string, ApiResult<decimal>>("DeleteAccount", (c, id) => c.DeleteAccount(id).Result);

    var inputs = new InputSet
    {
        spec.GetOperation<string, ApiResult<decimal>>("CreateAccount").With("alice", "Create alice"),
        spec.GetOperation<(string, decimal), ApiResult<decimal>>("Deposit").With(("alice", 100m), "Deposit 100"),
        spec.GetOperation<(string, decimal), ApiResult<decimal>>("Withdraw").With(("alice", 30m), "Withdraw 30"),
    };

    var testCases = spec.GenerateTests(new BankState(), inputs, new TestGenerationOptions { MaxDepth = 4 });

    var context = spec.CreateTestingContext();
    context.Register(new BankApiClient(CreateHttpClient()));

    var results = await spec.RunTests(context, new BankState(), testCases, new TestExecutionOptions
    {
        BeforeEachAsync = async _ => await ResetDatabase()
    });

    Assert.That(results.All(r => r.Success), $"Failed: {results.FirstOrDefault(r => !r.Success)?.LastFailureMessage}");
}
```

## Common Issues

### Too Many Test Cases

- Reduce `MaxDepth`
- Add `StateConstraint` to prune uninteresting states
- Use fewer inputs or more targeted inputs

### Tests Failing Unpredictably

- Ensure `BeforeEachAsync` properly resets state
- Check for timing issues (async operations may not complete)
- Verify inputs match what the spec expects

## Next Steps

- **Concurrency Testing**: Test race conditions with `GenerateConcurrentTests` and `RunTests`
- **Async Operations**: Handle background work with polling

More from microsoft/accordant

SkillDescription
accordant-async-operationsHow to model background work, step functions, and polling - use this skill when testing async workflows, job queues, or operations that complete in the background
accordant-concurrencyHow to test for race conditions using concurrent tests and linearizability - use this skill when testing concurrent operations or finding race condition bugs
accordant-cross-languageTesting 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.
accordant-operationsHow to define operations with Apply and Execute methods - use this skill when creating spec operations or understanding the Apply/Execute pattern
accordant-overviewWhat Accordant is, when to use model-based testing, and core concepts - use this skill when starting with Accordant or explaining its purpose
accordant-patternsCommon patterns in Accordant - response-dependent state, request derivations, error handling, and HTTP integration
accordant-quickrefQuick reference and cheatsheet for Accordant API - use this skill for quick syntax lookup or code snippets
accordant-stateHow to design state models in Accordant - use this skill when defining what your system tracks between operations
accordant-troubleshootingCommon mistakes and debugging tips for Accordant specs - use this skill when tests fail unexpectedly or specs don't behave as expected