microsoft/accordant
C#
Captured source
source ↗microsoft/accordant
Description: A model-based testing framework for .NET that validates implementations against behavioral specifications.
Language: C#
License: MIT
Stars: 4
Forks: 0
Open issues: 1
Created: 2026-03-16T03:09:13Z
Pushed: 2026-06-10T17:20:27Z
Default branch: main
Fork: no
Archived: no
README:
Executable behavioral specifications for .NET
Documentation · NuGet · [Samples](Samples/)
Accordant is a framework for model-based testing. You write a *spec* — executable code that captures the rules of your system. Given any state and any operation, the spec defines what the response should be and how the state should change. Accordant then generates hundreds of tests from this spec, runs them against your real implementation, and validates every response — telling you exactly where the implementation deviates from the rules.
---
How You Test Today
Say you're building a banking service. You write tests like this:
[Test]
public async Task Withdraw_WithSufficientBalance_Succeeds()
{
await client.CreateAccount("alice");
await client.Deposit("alice", 100);
var result = await client.Withdraw("alice", 30);
Assert.True(result.IsSuccess);
Assert.Equal(70, result.Data);
}Now imagine thirty more — insufficient balance, nonexistent account, duplicate creates, deposits after deletes, different orderings. Each test carries its own assertions, but they're all expressing pieces of the same *contract*: the rules for how your system behaves. Those rules end up scattered across your test suite, repeated in slightly different forms.
What if you wrote them once?
---
Extract the Contract
Here's the complete contract for Withdraw, in one place:
spec.Operation("Withdraw", (request, state) =>
{
if (!state.Accounts.TryGetValue(request.AccountId, out var balance))
return Expect.That(r => r.IsNotFound).SameState();
if (balance r.IsBadRequest).SameState();
var newBalance = balance - request.Amount;
return Expect.That(r => r.IsSuccess && r.Balance == newBalance)
.ThenState(s => s.Accounts[request.AccountId] = newBalance);
});spec.Operation registers an operation. The lambda receives the request and current state, and returns what the response should look like (Expect.That(...)) paired with how the state should change (.SameState() or .ThenState(...)). Read it as a truth table: account doesn't exist → not-found, state unchanged; insufficient balance → bad-request, state unchanged; otherwise → success with new balance, state updated.
The state is whatever information you need to define what correct behavior means. For banking, that's just accounts and their balances:
[State]
public partial class BankState
{
public Dictionary Accounts { get; set; } = new();
}The [State] attribute triggers source generation for cloning and equality — you define the data structure, Accordant handles the rest. The state is intentionally simpler than the real implementation. We treat the system as a black box; we don't care whether data lives in SQL Server, Redis, or a flat file.
→ [See the full BankAccount spec](Samples/BankAccount/)
---
What This Unlocks
Provide a few sample inputs — some account IDs, some amounts:
// Operation handles obtained via spec.GetOperation(name)
var inputs = new InputSet
{
createAccount.With(new CreateAccountRequest("alice"), "Create(alice)"),
deposit.With(new DepositRequest("alice", 50m), "Deposit(alice, 50)"),
deposit.With(new DepositRequest("alice", 100m), "Deposit(alice, 100)"),
withdraw.With(new WithdrawRequest("alice", 30m), "Withdraw(alice, 30)"),
withdraw.With(new WithdrawRequest("alice", 70m), "Withdraw(alice, 70)"),
deleteAccount.With(new DeleteAccountRequest("alice"), "Delete(alice)"),
};Because the spec defines what each operation does to state, Accordant can *simulate* the system — predicting what happens without running real code. Starting from an empty state, it tries every operation with every input. Operations that change the state produce new nodes (e.g., creating an account that doesn't exist); operations that don't change state loop back to the same node (e.g., withdrawing from a nonexistent account). From each new node, it tries every operation again. The reachable states naturally unfold into a graph:

Accordant then picks paths through this graph as test sequences:
| # | Sequence | Category | |---|----------|----------| | 1 | Create(alice) → Deposit(alice, 100) → Withdraw(alice, 70) | ✓ success path | | 2 | Create(alice) → Deposit(alice, 50) → Withdraw(alice, 70) | ✗ insufficient funds | | 3 | Withdraw(alice, 30) | ✗ 404 on non-existent account | | 4 | Create(alice) → Create(alice) | ✗ 409 duplicate | | 5 | Create(alice) → Deposit(50) → Delete(alice) | ↻ lifecycle | | 6 | Create(alice) → Deposit(100) → Deposit(50) | ✓ accumulate balance |
These aren't random — they're systematic walks designed to exercise different branches. Each sequence is run against the real system, and the spec validates every response along the way:
Generated 31 test cases Executed against BankAccount API Results: 31 passed, 0 failed
From six sample inputs, Accordant generated thirty-one test cases that cover the reachable state space, with every response validated automatically.
→ [How Test Generation Works](docs/concepts/how-test-generation-works.md)
It's Not Just Auto-Generation
The spec separates two concerns that traditional tests tangle together: deciding what sequence to run and deciding whether the result is correct. The spec handles the second part — it's an oracle. You bring whatever sequences you like.
That means you can pair the oracle with any source of test sequences:
- Hand-written scenarios that exercise specific edge cases
- Accordant's built-in state-graph exploration (what we just saw)
- Your own custom generation algorithms
- A fuzzer producing random operation streams
- Sequences replayed from production logs
In all cases, validation is the same single call:
var (isValid, message, nextState) = spec.Allows(operation, request, response, currentState);
This also improves your existing hand-written tests. Compare — today each test carries bespoke assertions that duplicate business logic:
var r1 = await…
Excerpt shown — open the source for the full document.