A modular API simulator library for .NET. Serve static JSON, load from files, or generate realistic responses with AI — all from a lightweight local web server in your tests.
| Package | Purpose | AI Required? |
|---|---|---|
| Augustus | Core simulator — static JSON, file-based, and custom response strategies with route matching | No |
| Augustus.AI | AI-powered response generation and real-API proxy mode via OpenAI | Yes |
| Augustus.APIs.Stripe | Pre-built Stripe API defaults and fluent helpers | No |
| Augustus.Reqnroll | Reqnroll (BDD) integration with per-scenario cache isolation | No |
dotnet add package Augustususing Augustus.Extensions;
public class StripeTests
{
[Fact]
public async Task Should_Return_Customer_Data()
{
var simulator = this.CreateAPISimulator("Stripe")
.ForGet("/v1/customers/{id}")
.WithJsonFile("./mocks/customer.json")
.Add()
.ForPost("/v1/charges")
.WithResponse(new { id = "ch_123", amount = 2000, currency = "usd", status = "succeeded" })
.Add();
await simulator.StartAsync();
var client = simulator.CreateClient();
var response = await client.GetAsync("/v1/customers/cus_123");
response.StatusCode.Should().Be(HttpStatusCode.OK);
var content = await response.Content.ReadAsStringAsync();
content.Should().Contain("cus_123");
await simulator.StopAsync();
}
}dotnet add package Augustus.AIusing Augustus.AI;
using Augustus.Extensions;
public class StripeTests
{
[Fact]
public async Task Should_Return_Customer_Data()
{
var simulator = this.CreateStripeSimulator(opt => opt.Port = 0);
simulator.UseAI(new AIOptions
{
OpenAIApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!
});
// Static override — always served from this JSON
simulator.ForGet("/v1/customers/cus_known")
.WithResponse(new { id = "cus_known", name = "John Doe" })
.Add();
// Everything else handled by AI
simulator.AddInstruction("Return realistic Stripe API responses");
await simulator.StartAsync();
var client = simulator.CreateClient();
// Hits the static route
var known = await client.GetStringAsync("/v1/customers/cus_known");
known.Should().Contain("John Doe");
// Falls through to AI
var generated = await client.GetStringAsync("/v1/customers/cus_other");
generated.Should().Contain("cus_other");
await simulator.StopAsync();
}
}using Augustus.AI;
simulator.ForGet("/v1/payments/{id}")
.UseAI(aiOptions, "Return a completed payment object")
.Add();
simulator.ForPost("/v1/payments")
.WithResponse(new { id = "pay_static", status = "pending" })
.Add();Requests are resolved in this order:
- Route with strategy — execute the matched
IResponseStrategy(static, file, AI, or custom) - Default handler — delegate to the AI or proxy handler (if installed via
UseAI/UseProxy) - No match, no default handler — HTTP 404 JSON error
var simulator = this.CreateAPISimulator("MyAPI", options =>
{
options.Port = 0; // 0 = auto-assign (default: 9001)
options.EnableCaching = true; // Cache AI/proxy responses (default: true)
options.CacheFolderPath = "./mocks"; // Where cache files live (default: ./mocks)
options.CacheOnly = false; // Serve only from cache, 503 on miss (default: false)
options.AutoRemoveStaleCache = true; // Clean up untouched cache files on dispose (default: true)
// Stable, renameable, hand-authorable fixtures (see "Cache identity" below)
options.NormalizeAzureOpenAIDeployment = true; // /openai/deployments/{x}/ → constant
options.RequestKeyTransform = key => key with { Path = "/v1/chat/completions" }; // rewrite volatile parts
options.IgnoredQueryParameters.Add("_cb"); // drop cache-buster params from the key
options.StripNullBodyProperties = true; // null vs omitted no longer churns
options.HashMessagesContentOnly = true; // only messages[].content keys the cache
options.OnCacheMiss = d => // discover the expected identity
Console.WriteLine($"miss: {d.ComputedKey} {d.ExpectedCanonicalRequest.Path}");
});simulator.UseAI(new AIOptions
{
OpenAIApiKey = "sk-...", // Required (unless CacheOnly)
OpenAIModel = "gpt-4o-mini", // Default: gpt-4o-mini
OpenAIEndpoint = "", // Optional: custom endpoint
UseAzureOpenAI = false, // Use Azure OpenAI service
AzureDeploymentName = "", // Required when UseAzureOpenAI = true
AzureApiVersion = "2024-06-01", // Azure API version
MaxRetries = 5, // Retry attempts for transient failures
MaxConcurrentRequests = 10 // Process-wide concurrent OpenAI limit (see below)
});Augustus.AI reduces duplicate traffic and 429 rate-limit pressure in several ways:
- Shared throttling — For a given API key (or Azure endpoint + key), model/deployment, and
MaxConcurrentRequestsvalue, all OpenAI chat completions in the process share one concurrency gate. This applies across the defaultUseAIhandler and route-levelUseAI, so parallel test classes do not each get a fullMaxConcurrentRequestsbudget in isolation. - In-flight deduplication — Concurrent requests that resolve to the same cache key (same method, path, query, normalized body, and route instructions) share a single in-flight OpenAI completion instead of fanning out.
- Retries — Transient failures (
429,5xx) use exponential backoff with jitter. When the service returns aRetry-Afterheader, the delay respects it (in addition to backoff caps fromInitialRetryDelayMs/MaxRetryDelayMs). - Proxy /
UseRealApi— Upstream HTTP calls retry on429and5xxwith the same delay settings; globalDynamicContentFieldsfromAPISimulatorOptionsare merged with per-routeWithDynamicFieldsfor cache keys. - Practical tips — For first-time cache generation, prefer a low
MaxConcurrentRequests(for example1–2) and/or run tests serially so retries do not multiply billed attempts. Use cache-only CI (CacheOnly/ committed mocks) so CI never calls OpenAI.
simulator.ForGet("/api/health")
.WithResponse(new { status = "ok" })
.WithStatusCode(200)
.Add();simulator.ForGet("/v1/customers/{id}")
.WithJsonFile("./mocks/customer.json")
.Add();simulator.ForPost("/api/echo")
.WithStrategy(new MyCustomStrategy())
.Add();Route-level UseAI uses the same structured cache keys as the default AI handler (CacheKeyComputer), stores entries in the simulator’s cache folder, and still reads legacy curl-based cache files if present. Azure OpenAI is supported on routes the same way as simulator.UseAI(...).
simulator.ForGet("/v1/payments/{id}")
.UseAI(aiOptions, "Return a completed payment with realistic fields")
.Add();simulator.ForPost("/v1/chat/completions")
.UseRealApi("https://api.openai.com", apiKey, aiOptions)
.Add();// Core (Augustus package) — no AI dependency
var sim = this.CreateAPISimulator("MyAPI");
var stripe = this.CreateStripeSimulator();
var paypal = this.CreatePayPalSimulator();
// AI-powered (Augustus.AI package)
var openai = this.CreateOpenAISimulator(opt => { opt.OpenAIApiKey = key; });
var azure = this.CreateAzureOpenAISimulator(opt => { ... });
// Proxies (Augustus.AI package)
var proxy = this.CreateOpenAIProxy(opt => { opt.OpenAIApiKey = key; });
var azureProxy = this.CreateAzureOpenAIProxy(opt => { ... });Forward requests to a real upstream API, cache responses, and replay on subsequent calls. Ideal for tool/function calling scenarios.
var simulator = this.CreateAPISimulator("OpenAI Proxy");
simulator.UseProxy(
new AIOptions
{
OpenAIApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!
},
upstreamUrl: "https://api.openai.com"
);
await simulator.StartAsync();
var client = simulator.CreateClient();
// First call: forwarded to real API, response cached
// Second call: served from cache instantly
var response = await client.PostAsync("/v1/chat/completions", content);Replay cached responses without an API key or network — perfect for CI/CD.
var simulator = this.CreateAPISimulator("Stripe", options =>
{
options.CacheOnly = true;
options.CacheFolderPath = "./pre-recorded-mocks";
});
await simulator.StartAsync();
// Returns cached responses or HTTP 503 on cache missdotnet add package Augustus.Reqnrollusing Augustus.AI;
using Augustus.Extensions;
using Augustus.Reqnroll;
using Reqnroll;
[Binding]
public class Hooks
{
private static APISimulator? _simulator;
[BeforeTestRun]
public static async Task BeforeTestRun()
{
_simulator = new Hooks().CreateStripeSimulator();
_simulator.UseAI(new AIOptions
{
OpenAIApiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY")!
});
_simulator.AddInstruction("Return realistic Stripe API responses");
AugustusReqnrollContext.Register(_simulator);
await _simulator.StartAsync();
}
}Cached responses are organized per scenario:
Features/
__mocks__/
Stripe/
Scenario_Name_1/
{hash}.json
Scenario_Name_2/
{hash}.json
MyFeature.feature
Cache keys are derived from the HTTP method, path, query string, and a canonical form of JSON request bodies (sorted object keys at every depth, plus optional DynamicContentFields normalization). Non-JSON bodies use raw bytes. See CHANGELOG.md for release notes when this algorithm changes — upgrades can invalidate existing on-disk caches.
Each fixture stores its CanonicalRequest, and matching is content-based: the file name is a free-form label, not the key. So you can:
- Rename or hand-author a fixture file to any name — it still resolves.
- Keep keys stable across incidental request changes with
NormalizeAzureOpenAIDeployment,RequestKeyTransform,IgnoredQueryParameters,StripNullBodyProperties, orHashMessagesContentOnly. - Use
OnCacheMiss(or the diagnostic fields in the503body) to discover the exact identity Augustus expected, so a fixture can be authored offline. - Re-baseline committed fixtures after a keying-rule change with zero upstream calls via
CacheMaintenance.Rekey(path, new RekeyOptions { … }).
Legacy fixtures without a CanonicalRequest keep resolving by their original hash file name, and the default key is unchanged — no rekey is needed unless you opt into a new rule.
Augustus caches responses as JSON files:
{
"RequestHash": "A1B2C3D4E5F6G7H8",
"Response": "{\"id\": \"cus_123\", \"name\": \"John Doe\"}",
"OriginalRequest": "curl -X GET http://localhost:9001/v1/customers/cus_123",
"Instructions": ["You are a Stripe API simulator"],
"Timestamp": "2024-01-15T10:30:00Z"
}simulator.ClearCache(); // Clear all cached responses
simulator.SetTestContext("my-scenario"); // Route cache to a subdirectory
simulator.ClearTestContext(); // Clear subdirectory and remove stale entriesAugustus (core) — no OpenAI dependency
└── Microsoft.AspNetCore.App (framework ref only)
Augustus.AI → Augustus
├── OpenAI (2.9.1)
└── Azure.AI.OpenAI (2.1.0)
Augustus.APIs.Stripe → Augustus
Augustus.Reqnroll → Augustus
└── Reqnroll packages
- Start with static mocks — use
WithResponse/WithJsonFilefor deterministic tests; add AI only where needed - Use
Port = 0— auto-assign ports to avoid conflicts in parallel test runs - Enable caching — dramatically speeds up AI-powered tests and reduces API costs
- Use cache-only in CI — commit cached responses and run without an API key
- Use
await using— ensures proper cleanup of the simulator's web server - Proxy mode for tool calling — when testing function/tool calling, use proxy mode to get real model reasoning
git clone https://github.com/chrisjainsley/augustus.git
cd augustus
dotnet build
dotnet testContributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.
If you encounter any issues or have questions:
- Search existing issues
- Create a new issue if needed
- Route-based response dispatch with pluggable strategies
- AI-powered response generation via OpenAI (Augustus.AI)
- Pass-through proxy mode with response caching
- Cache-only mode for CI/offline testing
- BDD integration (Reqnroll)
- Dynamic port assignment for parallel testing
- More API-specific packages (PayPal, Twilio, etc.)
- Support for more AI providers (Anthropic Claude, Google Gemini)
- Request/response validation
- Docker support