Generated Fakes (SignalRGen.Testing)
SignalRGen.Testing is a source generator that creates fake implementations for the strongly-typed hub clients generated by SignalRGen.
The goal: make UI/component tests and unit tests deterministic without running a real SignalR server and without mocking HubConnection APIs.
What gets generated (high level)
For each hub client you opt into, SignalRGen.Testing generates a fake class that:
- inherits from the production generated hub client (e.g.
FakeChatHubContractClient : ChatHubContractClient) - records client-to-server invocations (e.g.
SendMessageCalls) - provides
SimulateOn...Async(...)helpers to trigger server-to-client callbacks - provides
WaitForOn...Async(...)helpers to make tests non-flaky - optionally runs in Strict mode to throw on unconfigured / unsupported calls
- includes
ClearRecorded()to reset state between tests
Installation
Add the testing package to your test project:
dotnet add package SignalRGen.TestingEnabling fake generation
Fake generation is opt-in. You specify the hub clients you want fakes for using an assembly attribute:
using SignalRGen.Testing.Abstractions.Attributes;
[assembly: GenerateFakeForHubClient(typeof(ChatHubContractClient))]You can declare multiple:
using SignalRGen.Testing.Abstractions.Attributes;
[assembly: GenerateFakeForHubClient(typeof(ExampleHubClient))]
[assembly: GenerateFakeForHubClient(typeof(ChatHubContractClient))]Important: put this attribute in the test assembly (the project that compiles your tests).
Example: swapping production client → fake in DI
Because the fake inherits from the real generated client type, you can replace the production client registration with the fake while still injecting the production type.
Example (bUnit / component tests):
Services.AddSingleton<ChatHubContractClient, FakeChatHubContractClient>();Your production component/service continues to inject ChatHubContractClient, but at runtime it receives FakeChatHubContractClient.
Example: simulating server-to-client events (the “On…” handlers)
Production hub clients expose server-to-client callbacks as OnXxx delegates, for example:
ChatHubClient.OnUserJoined += HandleUserJoined;
ChatHubClient.OnUserLeft += HandleUserLeft;
ChatHubClient.OnMessageReceived += HandleMessageReceived;The fake adds simulation helpers that do two useful things:
- record the event payload into
On...Events - invoke the real
On...delegate if it is subscribed
Example:
await fakeClient.SimulateOnUserJoinedAsync("NewUser");Making the test non-flaky: WaitForOn…Async()
If the callback results in async UI updates (Blazor render, state change, etc.), you want a deterministic “the event was observed” signal.
That’s what WaitForOnUserJoinedAsync() is for:
await fakeClient.SimulateOnUserJoinedAsync("NewUser");
// Guarantees the fake observed the event publication (helps avoid timing flakes)
await fakeClient.WaitForOnUserJoinedAsync();
// Now assert on the UI/state
await cut.WaitForStateAsync(() =>
cut.FindAll(".msg").Any(m => m.TextContent.Contains("User 'NewUser' joined")));Available (example-based) helpers typically look like:
SimulateOnUserJoinedAsync(...)/WaitForOnUserJoinedAsync()SimulateOnUserLeftAsync(...)/WaitForOnUserLeftAsync()SimulateOnMessageReceivedAsync(...)/WaitForOnMessageReceivedAsync()
Example: asserting client-to-server calls (*Calls)
For each client-to-server hub method, the fake:
- intercepts the invocation
- records arguments (e.g.
SendMessageCalls) - optionally invokes a handler if you set one
Example assertion:
cut.Find("#message-input").Input("My secret message");
await cut.Find("#send-button").ClickAsync();
Assert.Contains("My secret message", fakeClient.SendMessageCalls);This is especially handy for component tests where you want to verify both:
- “did we call the hub?” and
- “did the UI update?”
Optional behavior: SendMessageHandler
If you want custom behavior when a method is invoked (e.g. trigger a callback, validate args, simulate server echo), you can configure the generated handler:
fakeClient.SendMessageHandler = async (message, ct) =>
{
// your custom behavior here
await Task.CompletedTask;
};Strict mode
Each fake exposes:
public bool Strict { get; set; }When Strict = true:
- invoking an unsupported hub method name throws
NotSupportedException - invoking a supported method with no configured behavior may throw
InvalidOperationException
Use strict mode when you want tests to fail loudly if your production code starts calling new hub methods and your tests didn’t account for it.
Resetting between tests: ClearRecorded()
Fakes record calls and events. If you reuse a fake instance (or share DI scope), you can reset everything:
fakeClient.ClearRecorded();This clears:
SendMessageCallsOnUserJoinedEvents,OnUserLeftEvents,OnMessageReceivedEvents- event wait channels (so
WaitFor...Async()only observes future events)
Lifecycle notes
- The fake overrides
StartAsync()/StopAsync()to avoid real networking. In typical tests you just call the UI/service code and let it callStartAsync(). - Prefer
await using(or async disposal patterns) in tests if your test framework supports it, because hub clients are async-disposable.
Recommended test pattern (quick checklist)
- Replace DI registration:
ChatHubContractClient -> FakeChatHubContractClient - Render/construct SUT
- Drive actions (click, input, call service method)
- Simulate server events with
SimulateOn…Async() - Wait deterministically using
WaitForOn…Async() - Assert on:
- recorded calls (
*Calls) - UI/state
- recorded calls (
- Optionally reset with
ClearRecorded()if reusing instances
