| name | accordant-troubleshooting |
| description | Common mistakes and debugging tips for Accordant specs - use this skill when tests fail unexpectedly or specs don't behave as expected |
Troubleshooting Accordant
This skill covers common mistakes, debugging techniques, and how to fix typical issues.
Test Failures
"No matching expected outcome"
The response doesn't match any Expect.That() predicate.
Diagnosis:
var (isValid, message, _) = spec.Allows(operation, request, actualResponse, stateProfile);
Console.WriteLine(message);
Common causes:
- Predicate too strict: Response has extra fields or slightly different values
- Wrong state: The spec thinks it's in state A, but the system is in state B
- Missing error case: You didn't handle an error condition in Apply
Fix:
Expect.That<ApiResult<User>>(
r => r.IsSuccess && r.Data.Name == expectedName,
$"Expected success with name '{expectedName}', got status={r?.StatusCode}, name='{r?.Data?.Name}'")
"State mismatch after operation"
Tracked state diverges from actual system state.
Common causes:
- Forgot to reset state: Previous test left data behind
- Missing ThenState: Operation modifies state but spec uses
.SameState()
- Wrong state update: ThenState modifies wrong fields
Fix:
var results = await spec.RunTests(context, initialState, testCases, new TestExecutionOptions
{
BeforeEachAsync = async _ =>
{
await client.DeleteAll();
var count = await client.GetCount();
if (count != 0) throw new Exception("Reset failed!");
}
});
Tests Pass Individually, Fail in Sequence
Common causes:
- Shared mutable state in test fixtures
- Incomplete reset between tests
- Timing issues with async operations
Fix:
var results = await spec.RunTests(context, initialState, testCases, new TestExecutionOptions
{
BeforeEachAsync = async ctx =>
{
var httpClient = _factory.CreateClient();
ctx.Context.Register(new ApiClient(httpClient));
await ResetDatabase();
}
});
State Issues
Modifying Original State
spec.Operation<string, ApiResult<decimal>>("Deposit", (accountId, state) =>
{
state.Accounts[accountId] += 100;
return Expect.That(...).SameState();
});
spec.Operation<string, ApiResult<decimal>>("Deposit", (accountId, state) =>
{
var newBalance = state.Accounts[accountId] + 100;
return Expect.That<ApiResult<decimal>>(r => r.Data == newBalance)
.ThenState<BankState>(nextState => nextState.Accounts[accountId] = newBalance);
});
State Class Missing [State] Attribute
public class BankState
{
public Dictionary<string, decimal> Accounts { get; set; }
}
[State]
public partial class BankState
{
public Dictionary<string, decimal> Accounts { get; set; } = new();
}
Nested State Not Marked
[State]
public partial class AppState
{
public Dictionary<string, UserState> Users { get; set; }
}
public class UserState { ... }
[State]
public partial class AppState
{
public Dictionary<string, UserState> Users { get; set; } = new();
}
[State]
public partial class UserState
{
public string Name { get; set; } = string.Empty;
}
Execution Issues
Operations Not Bound
System.InvalidOperationException: No execution binding found for operation 'CreateAccount'
Fix:
spec.ExecuteWith<ApiClient>()
.Bind<string, ApiResult<decimal>>("CreateAccount",
(client, accountId) => client.CreateAccount(accountId).Result);
Type Mismatch in Binding
spec.Operation<(string, decimal), ApiResult<decimal>>("Withdraw", ...);
spec.ExecuteWith<ApiClient>()
.Bind<string, ApiResult<decimal>>("Withdraw", ...);
spec.ExecuteWith<ApiClient>()
.Bind<(string, decimal), ApiResult<decimal>>("Withdraw",
(client, req) => client.Withdraw(req.Item1, req.Item2).Result);
Async/Await Issues
spec.ExecuteWith<ApiClient>()
.Bind<string, ApiResult<User>>("GetUser",
(client, id) => client.GetUserAsync(id).Result);
spec.ExecuteWith<ApiClient>()
.BindAsync<string, ApiResult<User>>("GetUser",
async (client, id) => await client.GetUserAsync(id));
Test Generation Issues
Too Many Test Cases
Diagnosis: Generation produces thousands of tests
Fixes:
var options = new TestGenerationOptions
{
MaxDepth = 3,
StateConstraint = state =>
{
var s = (AppState)state;
return s.Users.Count <= 2 &&
s.Users.Values.Sum(u => u.Todos.Count) <= 4;
}
};
No Test Cases Generated
Diagnosis: testCases.Count == 0
Common causes:
- Initial state doesn't allow any operation
- StateConstraint rejects initial state
- Empty InputSet
Fix:
var initialState = new AppState();
Assert.True(options.StateConstraint?.Invoke(initialState) ?? true, "Initial state rejected!");
Assert.That(inputs.Count, Is.GreaterThan(0));
State Space Explosion
Symptoms: Generation hangs or runs out of memory
Fix:
var options = new TestGenerationOptions
{
MaxDepth = 4,
StateConstraint = state =>
{
var s = (AppState)state;
return s.Items.Count <= 3;
},
SequentialTestCaseAlgorithm = SequentialTestCaseAlgorithms.CreateRandomWalk(
numberOfWalks: 100,
maxWalkLength: 5,
seed: 42)
};
Concurrency Test Issues
All Concurrent Tests Fail
Common cause: Implementation has no concurrency control — race conditions everywhere
Diagnosis: Check if sequential tests pass first
var seqResults = await spec.RunTests(context, initialState, testCases);
Assert.That(seqResults.All(r => r.Success), "Fix sequential tests first!");
var concTestCases = spec.GenerateConcurrentTests(initialState, inputs, options);
var concResults = await spec.RunTests(context, initialState, concTestCases);
Non-Deterministic Failures
Symptoms: Same test sometimes passes, sometimes fails
Common causes:
- Race conditions (the bug you're looking for!)
- Incomplete state reset
- External system interference
Diagnosis:
for (int i = 0; i < 10; i++)
{
await ResetState();
var concTestCases = spec.GenerateConcurrentTests(initialState, inputs, options);
var result = await spec.RunTests(context, initialState, concTestCases);
Console.WriteLine($"Run {i}: {(result.All(r => r.Success) ? "PASS" : "FAIL")}");
}
Async Operation Issues
Polling Never Terminates
Symptoms: Test hangs on async operation
Diagnosis:
var state = GetCurrentState();
var isTerminal = stepFunction.IsTerminal(state);
Console.WriteLine($"IsTerminal: {isTerminal}, State: {state}");
Common causes:
- isTerminal predicate is wrong — never returns true
- Background work never completes — liveness bug
- Wrong state being checked
Fix:
.Triggers(AsyncOperation.Create<JobQueueState>(
isTerminal: s =>
{
var status = s.Jobs.GetValueOrDefault(jobId)?.Status;
Console.WriteLine($"Checking terminal: status={status}");
return status != JobStatus.Pending;
},
...
))
State Profile Has Too Many States
Symptoms: After async operation, many possible states tracked
This is expected! Non-determinism means multiple outcomes are valid. Observations narrow it down.
var response = await client.GetJob(jobId);
(_, _, stateProfile) = spec.Allows(getJobOp, jobId, response, stateProfile);
Console.WriteLine($"Remaining possible states: {stateProfile.StatesAndStepFunctions.Count}");
Debugging Tips
Enable Detailed Logging
var results = await spec.RunTests(context, initialState, testCases, new TestExecutionOptions
{
OnStepExecuted = info =>
{
foreach (var (op, req, resp) in info.Operations)
Console.WriteLine($"{op.Name}({req}) → {resp}");
}
});
Visualize the State Graph
var dot = spec.VisualizeStateSpace(initialState, inputs, options);
File.WriteAllText("debug-graph.dot", dot);
Inspect a Single Test Case
var failedCase = testCases.First(tc => !results[tc].Success);
Console.WriteLine($"Failed sequence:");
foreach (var step in failedCase.Steps)
{
Console.WriteLine($" {step.OperationName}({step.Request})");
}
Manually Replay a Sequence
var stateProfile = new StateProfile(initialState);
foreach (var step in failedCase.Steps)
{
Console.WriteLine($"\n=== {step.OperationName} ===");
Console.WriteLine($"Request: {step.Request}");
Console.WriteLine($"Current states: {stateProfile.StatesAndStepFunctions.Count}");
var response = await ExecuteStep(step);
Console.WriteLine($"Response: {response}");
var (isValid, message, newProfile) = spec.Allows(step.Operation, step.Request, response, stateProfile);
Console.WriteLine($"Valid: {isValid}");
if (!isValid) Console.WriteLine($"Error: {message}");
stateProfile = newProfile;
}
Getting Help
- Check the samples:
Samples/ folder has working examples
- Read the concepts docs:
docs/concepts/ explains the theory
- Visualize: State graphs often reveal the problem
- Simplify: Reduce to minimal failing case