C# Source Generator for Strongly-Typed Mocks and Fakes
$ dotnet add package SatorImaging.TDoubles🇺🇸 English ❘ 🇯🇵 日本語版 ❘ 🇨🇳 简体中文版

TDoubles is a powerful C# source generator that revolutionizes unit testing by creating mock wrapper classes at compile-time. Instead of relying on complex runtime reflection or proxy generation like traditional mocking frameworks, this generator produces clean, readable C# code during compilation that wraps your target types with customizable behavior.
| Feature | TDoubles | Traditional Frameworks (Moq, NSubstitute) |
|---|---|---|
| Performance | Zero runtime overhead, compile-time generation | Runtime reflection and proxy creation |
| Type Safety | Full compile-time checking and IntelliSense | Runtime configuration, limited IntelliSense |
| Generic Support | Full support including constraints | Limited generic type support |
| Setup Complexity | Single attribute, minimal configuration | Complex fluent APIs and setup expressions |
| Debugging | Generated code is readable and debuggable | Proxy objects can be difficult to debug |
Apply the [Mock] attribute to a partial class, and the generator handles the rest.
using TDoubles;
public interface IDataService
{
string GetData(int id);
void SaveData(string data);
}
[Mock(typeof(IDataService))] // 👈
partial class DataServiceMock
{
// Implementation will be generated automatically
}
Here shows how to use the mock in your code.
// Create the mock
var mockService = new DataServiceMock();
// Override behavior for testing
mockService.MockOverrides.GetData = (id) => $"MockData_{id}";
// ~~~~~~~~~~~~~
string mockData = mockService.GetData(123); // Returns "MockData_123"
You can delegate to real implementation and override partial behaviour of the mock.
var mock = new DataServiceMock(new ConcreteDataService());
// Use default behavior (delegates to real service)
var realData = mock.GetData(123);
// Override partial behaviour for testing
mock.MockOverrides.SaveData = (data) => Console.WriteLine($"Saved: {data}");
mock.SaveData(realData);
Implements fake behaviors for debugging purposes in conjunction with latest update to the real implementation.
[Mock(typeof(IFoo), nameof(IFoo.Save), nameof(IFoo.Load))]
partial class FooFake
{
public void Save() => File.WriteAllText("...", JsonUtility.ToJson(this, true));
public void Load() => JsonUtility.FromJsonOverwrite(File.ReadAllText("..."), this);
}
// Delegates to latest ConcreteFoo implementation except for Save and Load
var fake = new FooFake(new ConcreteFoo());
TDoubles provides support for generic type mocking on both unbound and closed constructed generics.
[Mock(typeof(IList<int>))]
partial class ListIntMock {}
// Proper type constraint for TKey is automatically generated and
// type parameter naming mismatch is also resolved
[Mock(typeof(IDictioanry<,>))]
partial class DictionaryMock<T, U> {}
There are efficient extension points to implement custom callback for each mock member call.
[!TIP] As C# specification,
partial voidmethod call is completely removed from built assembly when method body is not implemented in your mock class declaration.https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/partial-member
[Mock(typeof(IList<>))]
partial class ListSpy<T> // 🕵 < Investigate suspects!
{
readonly Dictionary<string, int> _callCountByName = new();
// Without allocating object?[] instance
partial void OnWillMockCall(string memberName)
{
if (!_callCountByName.TryGetValue(memberName, out var current))
{
current = 0;
}
_callCountByName[memberName] = current + 1;
}
// Another overload can take arguments passed to mock member
// * Array.Empty<object>() is used for parameterless members
partial void OnWillMockCall(string memberName, object?[] args)
{
// How to determine method overload
if (memberName == "Add")
{
if (args[0] is T)
{
Console.WriteLine("Add(T item) is invoked.");
}
else
{
Console.WriteLine("Add(object item) is invoked.");
}
}
}
}
Mock Attribute OptionsThere are options to select generated mock members.
// Include internal types, interfaces and members to mock generation
[Mock(typeof(Foo), IncludeInternals = true)]
partial class FooMock { }
// Exclude specified members from mock generation (no error if member is not found)
[Mock(typeof(Foo), "ToString", "Foo", "Bar", IncludeInternals = false)]
partial class FooMockWithoutToStringOverride
{
// You can re-implement excluded 'ToString' as you desired
public override string ToString() => base.ToString() ?? "<NULL>";
}
The generator works by analyzing types marked with the [Mock] attribute and generating corresponding mock classes that delegate to the original implementation while providing override capabilities through a simple, strongly-typed API. This approach eliminates the performance overhead of reflection-based mocking while maintaining full type safety and IntelliSense support.
[Mock] attribute to a partial class, and the generator handles the restIncludeInternals configuration allows mocking of internal members for comprehensive testingTDoubles generator excels in scenarios where you need:
[Mock(typeof(TargetType))] attribute to a partial classMockOverrides for custom behaviorHere is pseudo code of delegation. Actual code is more complicated as need to support ref and out parameter modifiers.
public string GetData(int id)
{
// Returns 'default' if value type or nullable reference type otherwise throws
return MockOverrides.GetData?.Invoke(id)
?? _target?.GetData(id)
?? throw new TDoublesException(...);
}
When you create a mock class, the generator adds several members:
[Mock(typeof(IUserService))]
partial class UserServiceMock
{
// Generated by source generator:
// Constructor that takes the target instance
public UserServiceMock(IUserService? target = default) { }
// Access to the underlying target
public IUserService? MockTarget { get; }
// Unified callback
partial void OnWillMockCall(string memberName);
partial void OnWillMockCall(string memberName, object?[] args);
// Override configuration object
public sealed class MockOverrideContainer { }
public MockOverrideContainer MockOverrides { get; }
// All interface/class members are implemented
public string GetUserName(int userId) { /* generated implementation */ }
public Task<bool> DeleteUser(int userId) { /* generated implementation */ }
// ... etc
}
Install-Package SatorImaging.TDoubles
dotnet add package SatorImaging.TDoubles
Add the following to your project file (.csproj):
<PackageReference Include="SatorImaging.TDoubles" Version="1.0.0" />
[Mock] attribute to generate mocksNo additional project configuration is required. The source generator automatically activates when the package is installed and will generate mock classes during compilation.
To verify the installation was successful:
using TDoubles;
public interface ITestService
{
string GetMessage();
}
[Mock(typeof(ITestService))]
partial class TestServiceMock
{
// Mock implementation will be generated here
}
dotnet build and msbuildThis section provides step-by-step examples to get you started with TDoubles. All examples are complete and ready to use in your projects.
Before using the TDoubles generator, ensure your mock classes meet these requirements:
partial[Mock(typeof(TargetType))] to the partial classusing TDoubles;The most common scenario is mocking interfaces for dependency injection testing.
using TDoubles;
using System;
using System.Threading.Tasks;
// Define your interface
public interface IUserService
{
string GetUserName(int userId);
Task<bool> DeleteUser(int userId);
bool IsUserActive(int userId);
}
// Create a partial mock class
[Mock(typeof(IUserService))]
partial class UserServiceMock
{
// The source generator will create the complete implementation here
}
// Example usage in tests
class Program
{
static void Main()
{
// Create a concrete implementation for delegation
var realService = new ConcreteUserService();
// Create the mock with the real service as the underlying target
var mockService = new UserServiceMock(realService);
Console.WriteLine("=== Default Behavior (Delegates to Real Service) ===");
Console.WriteLine($"User Name: {mockService.GetUserName(123)}");
Console.WriteLine($"Is Active: {mockService.IsUserActive(123)}");
Console.WriteLine("\n=== Custom Behavior with Overrides ===");
// Override specific methods for testing
mockService.MockOverrides.GetUserName = (userId) => $"MockUser_{userId}";
mockService.MockOverrides.IsUserActive = (userId) => userId > 100;
Console.WriteLine($"User Name (Overridden): {mockService.GetUserName(123)}");
Console.WriteLine($"Is Active (Overridden): {mockService.IsUserActive(50)}");
Console.WriteLine($"Is Active (Overridden): {mockService.IsUserActive(150)}");
// Access the underlying real service if needed
Console.WriteLine($"Real Service: {mockService.MockTarget.GetUserName(123)}");
}
}
// Concrete implementation for demonstration
public class ConcreteUserService : IUserService
{
public string GetUserName(int userId) => $"RealUser_{userId}";
public async Task<bool> DeleteUser(int userId) => await Task.FromResult(true);
public bool IsUserActive(int userId) => true;
}
Mock concrete classes to test inheritance scenarios and virtual method overrides.
using TDoubles;
using System;
// Base service class with virtual methods
public class DatabaseService
{
public virtual string GetConnectionString() => "Server=localhost;Database=prod;";
public virtual void SaveData(string data) => Console.WriteLine($"Saving to database: {data}");
public virtual int GetRecordCount() => 1000;
// Non-virtual method (will be wrapped but not overridable)
public string GetServiceName() => "DatabaseService";
}
// Create mock for the class
[Mock(typeof(DatabaseService))]
partial class DatabaseServiceMock
{
// Generated implementation will wrap all public methods
}
// Example usage
class Program
{
static void Main()
{
// Create real service instance
var realService = new DatabaseService();
// Create mock wrapper
var mockService = new DatabaseServiceMock(realService);
Console.WriteLine("=== Default Behavior ===");
Console.WriteLine($"Connection: {mockService.GetConnectionString()}");
Console.WriteLine($"Service Name: {mockService.GetServiceName()}");
Console.WriteLine($"Record Count: {mockService.GetRecordCount()}");
mockService.SaveData("test data");
Console.WriteLine("\n=== Testing Scenario Overrides ===");
// Override for testing scenarios
mockService.MockOverrides.GetConnectionString = () => "Server=testserver;Database=test;";
mockService.MockOverrides.GetRecordCount = () => 0; // Simulate empty database
mockService.MockOverrides.SaveData = (data) => Console.WriteLine($"TEST MODE: Would save '{data}'");
Console.WriteLine($"Test Connection: {mockService.GetConnectionString()}");
Console.WriteLine($"Test Record Count: {mockService.GetRecordCount()}");
mockService.SaveData("test data");
// Non-virtual methods still work but delegate to original
Console.WriteLine($"Service Name (always delegates): {mockService.GetServiceName()}");
}
}
Mock classes that both inherit from base classes and implement interfaces.
using TDoubles;
using System;
// Interface definition
public interface INotificationService
{
void SendNotification(string message);
bool IsServiceAvailable();
}
// Base class with virtual methods
public class BaseService
{
public virtual string GetServiceType() => "Base";
public virtual void Initialize() => Console.WriteLine("Base initialization");
}
// Concrete class that inherits and implements interface
public class EmailService : BaseService, INotificationService
{
public override string GetServiceType() => "Email";
public override void Initialize() => Console.WriteLine("Email service initialized");
public void SendNotification(string message) => Console.WriteLine($"Email: {message}");
public bool IsServiceAvailable() => true;
}
// Mock the concrete class
[Mock(typeof(EmailService))]
partial class EmailServiceMock
{
// Mocks both inherited methods and interface implementations
}
// Usage example
class Program
{
static void Main()
{
var realService = new EmailService();
var mockService = new EmailServiceMock(realService);
Console.WriteLine("=== Testing Inherited Methods ===");
Console.WriteLine($"Service Type: {mockService.GetServiceType()}");
mockService.Initialize();
Console.WriteLine("\n=== Testing Interface Methods ===");
mockService.SendNotification("Hello World");
Console.WriteLine($"Available: {mockService.IsServiceAvailable()}");
Console.WriteLine("\n=== Testing with Overrides ===");
// Override inherited method
mockService.MockOverrides.GetServiceType = () => "MockEmail";
mockService.MockOverrides.Initialize = () => Console.WriteLine("Mock initialization");
// Override interface methods
mockService.MockOverrides.SendNotification = (msg) => Console.WriteLine($"MOCK EMAIL: {msg}");
mockService.MockOverrides.IsServiceAvailable = () => false;
Console.WriteLine($"Service Type: {mockService.GetServiceType()}");
mockService.Initialize();
mockService.SendNotification("Test Message");
Console.WriteLine($"Available: {mockService.IsServiceAvailable()}");
}
}
For advanced scenarios including generic types, static classes, records, structs, and internal member access, see the Advanced Usage Guide.
For comprehensive testing examples with MSTest, NUnit, and performance comparisons, see the Testing Examples Guide.
record and record structIEquatable<MOCK_TARGET_RECORD> and MockOverrides.MockTargetRecord_Equals
IEquatable<GENERATED_MOCK>bool Equals(object?) cannot be overriddenWhen method uses method-level type parameter instead of type-level parameter, MockOverrides will use object instead of method-level type parameter.
// Generated mock has type-level parameter T
partial class Mock<T>
{
// Generated mock method that has T and TMethod type parameter
public TMethod GenericMethod<T, TMethod>(T input) { ... }
// <TMethod> can be added to this class but it must also be exposed as type-level parameter...
public sealed class MockOverrideContainer
{
// type-level parameter T is used but TMethod is shadowed to object
public Func<T, object> GenericMethod { get; set; }
// ~~~~~~ Not TMethod
}
}
[!NOTE] Generated mock method returns
TMethodas mock target does. Internally, mock method will castobjectresult from override toTMethodwhen returning value.
Unsupported Types:
int, string, etc.)object, ValueType, Enum and other special types such as Span<T>Unsupported Constraints:
where T : defaultwhere T : allows ref structUnsupported Type:
ref return typeAttributes on type, method, property or etc are not preserved in generated mock.
Unsupported Members:
ref and out parameters in some complex scenarios (?)__arglist (variable arguments)Partial Support:
public interface IService
{
// ✅ Fully supported
string GetData(int id);
Task<bool> ProcessAsync(string data);
// ⚠️ Limited support - may not override correctly
ref int GetReference();
void ProcessData(__arglist);
}
Some valid type constraint is not transformed correctly. We have no plan to support this edge case of type constraint.
Note:
overridemethod cannot have type constraint except forclassandstruct.
// Abstract method declaration that returns (M, N?) with where M : N? constraint
public abstract (M t, N? u) TypeArgMappingNullable_Abstract<M, N>() where M : N?;
// Expected (valid) return type is (M, N)
public override (M t, N u) TypeArgMappingNullable_Abstract<M, N>() { }
// But got (M, N?)
public override (M t, N? u) TypeArgMappingNullable_Abstract<M, N>() { }
Multiple Interface Implementation:
Virtual Method Overriding:
virtual and abstract methods can be overridden in class mockssealed methods cannot be overridden (will delegate to original)Framework Support:
IDE Integration:
We welcome and appreciate contributions from the community! Whether you're fixing bugs, adding features, improving documentation, or providing feedback, your contributions help make TDoubles better for everyone.
See CONTRIBUTING.md
We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and professional in all interactions.
If you encounter issues not covered in this troubleshooting guide:
Support Channels:
If you discover a security vulnerability, please report it privately by emailing the maintainers rather than creating a public issue. This allows us to address the issue before it becomes widely known.
static class mockingsealed overridden methodsasync testsevent getter and setter testsreadonly struct testsreadonly record struct testsTuple and ValueTuple tests{ get; private set; } or etc)ref return[Mock(typeof(Foo.Bar))])[Mock(typeof(Foo.NestedKeyValueStore<,>))])<inheritdoc cref="..." /> for mock membersdefault and allows ref struct type constraint
default constraint is valid on override and explicit interface implementation methods onlyImmutableArray<T> or ImmutableList<T> as possibleStringBuilder useMock attribute option to generate MockCallCounts that records the call count of each mock membervolatile int fieldsInterlocked.Increment(ref ...) method at the beginning of generated mock class member.Sator Imaging
We thank all contributors who have helped improve this project through code contributions, bug reports, feature suggestions, and community support.
This project is licensed under the MIT License.
This project uses the following third-party packages:
© 2025 Sator Imaging. All rights reserved.
For support, questions, or contributions, please visit our GitHub repository.