Tutorial 3: Custom Strategies
This tutorial covers the different ways to provide custom strategies for [Property] test parameters.
Automatic Resolution
By default, Conjecture resolves strategies from parameter types. This works for:
- All
IBinaryInteger<T>types:int,long,byte,short,uint, etc. bool,double,float,stringT?whereT : structList<T>,IReadOnlySet<T>,IReadOnlyDictionary<TKey, TValue>(T1, T2),(T1, T2, T3),(T1, T2, T3, T4)tuples- Enums
For any type Conjecture can't auto-resolve, you need to provide a strategy.
Option 1: IStrategyProvider<T> with [From<T>]
Create a class that implements IStrategyProvider<T>:
public class EmailAddress : IStrategyProvider<string>
{
public Strategy<string> Create() =>
from local in Generate.Strings(minLength: 1, maxLength: 20)
from domain in Generate.SampledFrom(new[] { "example.com", "test.org", "mail.net" })
select $"{local}@{domain}";
}
Apply it to a parameter:
[Property]
public bool Emails_contain_at_sign([From<EmailAddress>] string email)
{
return email.Contains('@');
}
Providers for Your Own Types
For domain types, the provider can live alongside the type:
public record Money(decimal Amount, string Currency);
public class MoneyStrategy : IStrategyProvider<Money>
{
public Strategy<Money> Create() =>
from amount in Generate.Integers<int>(0, 100_000).Select(x => (decimal)x / 100)
from currency in Generate.SampledFrom(new[] { "USD", "EUR", "GBP", "NOK" })
select new Money(amount, currency);
}
[Property]
public bool Money_amount_is_non_negative([From<MoneyStrategy>] Money money)
{
return money.Amount >= 0;
}
Option 2: [FromFactory] — Static Factory Methods
If you prefer keeping the strategy close to the test, use a static factory method:
public class PaymentTests
{
static Strategy<Money> PositiveMoney() =>
from amount in Generate.Integers<int>(1, 100_000).Select(x => (decimal)x / 100)
from currency in Generate.SampledFrom(new[] { "USD", "EUR" })
select new Money(amount, currency);
[Property]
public bool Payment_preserves_currency([FromFactory("PositiveMoney")] Money money)
{
var payment = new Payment(money);
return payment.Amount.Currency == money.Currency;
}
}
The method must be static, return Strategy<T>, and be defined on the test class.
Option 3: [Arbitrary] Source Generator
For types you control, add [Arbitrary] to auto-derive a strategy at compile time:
[Arbitrary]
public partial record Point(double X, double Y);
The source generator creates a PointArbitrary : IStrategyProvider<Point> class. Use it with [From<PointArbitrary>]:
[Property]
public bool Distance_is_non_negative(
[From<PointArbitrary>] Point a,
[From<PointArbitrary>] Point b)
{
return Distance(a, b) >= 0;
}
See How to use source generators for details.
Composing Providers
Providers can use other strategies internally:
public class NonEmptyListOfMoney : IStrategyProvider<List<Money>>
{
public Strategy<List<Money>> Create() =>
Generate.Lists(new MoneyStrategy().Create(), minSize: 1, maxSize: 20);
}
When to Use What
| Approach | Best for |
|---|---|
| Automatic resolution | Primitive types, standard collections |
[From<T>] with IStrategyProvider<T> |
Reusable strategies shared across tests |
[FromFactory] |
One-off strategies specific to a test class |
[Arbitrary] source generator |
Types you own with simple constructor patterns |
Next
Tutorial 4: Shrinking Explained — understand how Conjecture minimizes counterexamples.