Tutorial 6: Advanced Patterns
This tutorial covers advanced Conjecture features: source generators, the example database, assembly-level settings, and testing patterns.
Source Generators
The source generator (bundled in Conjecture.Core) auto-derives strategies for your types at compile time.
Mark a type with [Arbitrary]:
using Conjecture.Core;
[Arbitrary]
public partial record Address(string Street, string City, int ZipCode);
The source generator creates AddressArbitrary : IStrategyProvider<Address> using auto-resolved strategies for each constructor parameter. Use it:
[Property]
public bool Addresses_have_street([From<AddressArbitrary>] Address addr)
{
return addr.Street is not null;
}
Requirements:
- The type must be
partial - Must have an accessible constructor
- All constructor parameter types must have resolvable strategies
See How to use source generators for details.
Roslyn Analyzers
Roslyn analyzers are bundled in Conjecture.Core and active automatically. Diagnostics:
| ID | Description |
|---|---|
| CON100 | Assertion inside void [Property] — consider returning bool |
| CON101 | Where() predicate likely rejects most values |
| CON102 | Sync-over-async (.Result, .GetAwaiter().GetResult()) inside [Property] |
| CON103 | Strategy bounds are inverted (min > max) |
| CON104 | Assume.That(false) always skips |
| CON105 | [Arbitrary] provider exists but [From<T>] not used |
The Example Database
When a test fails, Conjecture stores the failing byte buffer in a local SQLite database. Next time the test runs, it replays that input first — guaranteeing the bug is caught immediately without waiting for random generation to rediscover it.
The database lives at DatabasePath (default: .conjecture/examples/).
Workflow
- Test fails → failing example saved to database
- You fix the bug
- Test passes → example stays in database as a regression test
- Future runs replay stored examples before generating new ones
CI Considerations
The example database is a local file. In CI, you can:
- Commit it —
.conjecture/examples/in your repo ensures CI replays known failures - Disable it —
[assembly: ConjectureSettings(UseDatabase = false)]
Assembly-Level Settings
Override defaults for all tests in an assembly:
[assembly: ConjectureSettings(MaxExamples = 500, DatabasePath = ".test-db/")]
Per-test [Property] attributes take precedence over assembly-level settings.
Explicit Examples
[Example] runs specific inputs before generated ones:
[Property]
[Example("")]
[Example(" ")]
[Example("normal input")]
public bool Trim_handles_edge_cases(string input)
{
var trimmed = input.Trim();
return trimmed.Length <= input.Length;
}
Use this for:
- Known edge cases (empty, null, boundary values)
- Regression tests from past failures
- Documentation by example
Async Properties
Property methods can be async:
[Property]
public async Task<bool> Api_returns_valid_response(int id)
{
Assume.That(id > 0);
var response = await _client.GetAsync($"/api/items/{id}");
return response.StatusCode != System.Net.HttpStatusCode.InternalServerError;
}
Testing Patterns
Roundtrip / Serialization
[Property]
public bool Json_roundtrip(MyType value)
{
var json = JsonSerializer.Serialize(value);
var deserialized = JsonSerializer.Deserialize<MyType>(json);
return value == deserialized;
}
Invariants
[Property]
public bool Set_union_contains_both(
List<int> a,
List<int> b)
{
var union = new HashSet<int>(a);
union.UnionWith(b);
return a.All(union.Contains) && b.All(union.Contains);
}
Oracle / Reference Implementation
[Property]
public bool Custom_sort_matches_linq(List<int> items)
{
var expected = items.OrderBy(x => x).ToList();
var actual = MySort.Sort(items);
return expected.SequenceEqual(actual);
}
Idempotence
[Property]
public bool Normalize_is_idempotent(string input)
{
var once = Normalize(input);
var twice = Normalize(once);
return once == twice;
}
Further Reading
- Reference: Settings — all settings
- How to use source generators —
[Arbitrary]in depth - Reference: Analyzers — all diagnostic rules
- API Reference — generated docs