Tutorial 3: Response-Dependent State
Sometimes the server returns values that you need to track—like timestamps, ETags, or server-generated IDs. You can't know these values ahead of time, but subsequent operations depend on them. This tutorial shows you how to handle response-dependent state.
Time: 15-20 minutes
What you'll learn:
- Using
ThenStatewith a response lambda - Providing mock responses for state exploration
- Tracking server-generated values
Prerequisites:
- Completed Tutorial 1 and Tutorial 2
The Problem
Let's add a LastModified timestamp to our todos. The server sets this automatically:
{
"todoId": "task-1",
"title": "Buy milk",
"completed": false,
"lastModified": "2024-01-15T10:30:00Z" // Server-generated!
}
When we create a todo, we don't know what timestamp the server will return. But:
- We need to capture that timestamp
- Subsequent
GetTodocalls should validate the timestamp matches
The Solution: Response Lambda + Mock
Accordant handles this with a two-part approach:
Part 1: Capture with Response Lambda
When creating a todo, use .ThenState<TState> which passes both a cloned state and the response to your lambda:
spec.Operation<Todo, ApiResult<Todo>>("CreateTodo", (request, state) =>
{
if (!state.Users.TryGetValue(request.UserId, out var user))
{
return Expect.That<ApiResult<Todo>>(r => r.IsNotFound,
"User not found")
.SameState();
}
if (user.Todos.ContainsKey(request.TodoId))
{
return Expect.That<ApiResult<Todo>>(r => r.IsConflict,
"Todo already exists")
.SameState();
}
// Success - but we don't know LastModified yet!
return Expect.That<ApiResult<Todo>>(
r => r.IsSuccess &&
r.Data != null &&
r.Data.TodoId == request.TodoId &&
r.Data.LastModified != null, // Just verify it exists
"Should create todo with timestamp")
.ThenState<AppState>(
// Lambda receives response and clone, modifies the clone
(ApiResult<Todo> response, AppState nextState) =>
nextState.Users[request.UserId].Todos[request.TodoId] = new TodoState
{
Title = request.Title,
Completed = false,
LastModified = response.Data!.LastModified // Capture it!
},
// Mock for state space exploration (explained below)
mock: () => new ApiResult<Todo>
{
Data = new Todo(request.UserId, request.TodoId, request.Title)
{
LastModified = DateTime.UtcNow
},
StatusCode = 200
});
});
Part 2: Why the Mock?
Accordant does two things:
- State exploration - Builds the state graph to generate test cases
- Test execution - Runs against your real system
During exploration, there's no real server—so we need mock responses. The mock provides realistic values so the state graph is accurate.
During execution, the mock is ignored—real responses are used.
Validating Captured Values
Now GetTodo can validate the captured timestamp:
spec.Operation<(string UserId, string TodoId), ApiResult<Todo>>("GetTodo", (request, state) =>
{
var todo = state.Users.GetValueOrDefault(request.UserId)
?.Todos.GetValueOrDefault(request.TodoId);
if (todo == null)
{
return Expect.That<ApiResult<Todo>>(r => r.IsNotFound,
"Todo not found")
.SameState();
}
// Now we can validate the captured LastModified!
return Expect.That<ApiResult<Todo>>(
r => r.IsSuccess &&
r.Data != null &&
r.Data.TodoId == request.TodoId &&
r.Data.Title == todo.Title &&
r.Data.LastModified == todo.LastModified, // Must match!
$"Should return todo with LastModified={todo.LastModified}")
.SameState();
});
Updated State Class
Add LastModified to your state:
[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;
public Dictionary<string, TodoState> Todos { get; set; } = new();
}
[State]
public partial class TodoState
{
public string Title { get; set; } = string.Empty;
public bool Completed { get; set; } = false;
public DateTime? LastModified { get; set; } // Added!
}
Another Example: Server-Generated IDs
The same pattern works for server-generated IDs:
// Request doesn't include ID - server generates it
public record CreateOrderRequest(string Product, int Quantity);
spec.Operation<CreateOrderRequest, ApiResult<Order>>("CreateOrder", (request, state) =>
{
return Expect.That<ApiResult<Order>>(
r => r.IsSuccess &&
r.Data != null &&
!string.IsNullOrEmpty(r.Data.OrderId) && // Server generates
r.Data.Product == request.Product,
"Should create order with server-generated ID")
.ThenState<AppState>(
(ApiResult<Order> response, AppState nextState) =>
{
var orderId = response.Data!.OrderId; // Capture!
nextState.Orders[orderId] = new OrderState
{
Product = request.Product,
Quantity = request.Quantity
};
},
mock: () => new ApiResult<Order>
{
Data = new Order
{
OrderId = Guid.NewGuid().ToString(), // Mock generates ID
Product = request.Product,
Quantity = request.Quantity
},
StatusCode = 201
});
});
Temporal Properties: Stability
Some server-generated values should never change once set. For example, a result path for a completed job:
// First observation - capture the value
if (job.ResultPath == null && response.Data.Status == JobStatus.Completed)
{
return Expect.That<ApiResult<Job>>(
r => r.Data.ResultPath != null,
"Should have a ResultPath")
.ThenState<JobQueueState>(
(ApiResult<Job> resp, JobQueueState nextState) =>
nextState.Jobs[jobId].ResultPath = resp.Data!.ResultPath, // Capture
mock: () => new ApiResult<Job> { /* ... */ });
}
// Subsequent observations - enforce stability
if (job.ResultPath != null)
{
return Expect.That<ApiResult<Job>>(
r => r.Data.ResultPath == job.ResultPath, // Must match exactly!
$"ResultPath must remain stable: {job.ResultPath}")
.SameState();
}
This is a temporal property: once set, the value never changes. Accordant will generate tests that verify this stability.
Summary
Response-dependent state handles values you can't predict:
| Pattern | Use Case |
|---|---|
.ThenState<TState>((TResponse response, TState nextState) => ..., mock) |
Capture server-generated values |
| Mock responses | Enable state exploration without real server |
| Stability checks | Enforce values don't change unexpectedly |
Key Insight
The spec captures reality: "I don't know what timestamp the server will return, but once I see it, I'll remember it and expect it to be consistent."
What's Next?
- Tutorial 4: Visualizing State Space - See the state graph Accordant explores
- Tutorial 5: Testing Race Conditions - Find concurrency bugs
Full Code Reference
See response-dependent state in:
- JobQueueTests.cs -
ResultPathcapture pattern