Skip to content

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:

shell
dotnet add package SignalRGen.Testing

Enabling fake generation

Fake generation is opt-in. You specify the hub clients you want fakes for using an assembly attribute:

csharp
using SignalRGen.Testing.Abstractions.Attributes;

[assembly: GenerateFakeForHubClient(typeof(ChatHubContractClient))]

You can declare multiple:

csharp
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):

csharp
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:

csharp
ChatHubClient.OnUserJoined += HandleUserJoined;
ChatHubClient.OnUserLeft += HandleUserLeft;
ChatHubClient.OnMessageReceived += HandleMessageReceived;

The fake adds simulation helpers that do two useful things:

  1. record the event payload into On...Events
  2. invoke the real On... delegate if it is subscribed

Example:

csharp
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:

csharp
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:

csharp
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:

csharp
fakeClient.SendMessageHandler = async (message, ct) =>
{
    // your custom behavior here
    await Task.CompletedTask;
};

Strict mode

Each fake exposes:

csharp
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:

csharp
fakeClient.ClearRecorded();

This clears:

  • SendMessageCalls
  • OnUserJoinedEvents, 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 call StartAsync().
  • Prefer await using (or async disposal patterns) in tests if your test framework supports it, because hub clients are async-disposable.
  1. Replace DI registration: ChatHubContractClient -> FakeChatHubContractClient
  2. Render/construct SUT
  3. Drive actions (click, input, call service method)
  4. Simulate server events with SimulateOn…Async()
  5. Wait deterministically using WaitForOn…Async()
  6. Assert on:
    • recorded calls (*Calls)
    • UI/state
  7. Optionally reset with ClearRecorded() if reusing instances