bUnit: Components render too many times with SetParametersAndRender

In certain cases, calls to StateHasChanged behave differently in BUnit than in regular Blazor WASM (I haven’t tested server-side, but I’d expect it to have the same problem). This means that components in BUnit render too many times, and RenderCount values in BUnit are often higher than they should actually be. I have observed this when using SetParametersAndRender, but I’m not sure if there are other scenarios where this happens.

This is a problem in my case, because I care a lot about the number of renders, but I have no way to reliably assert the RenderCount in tests.

Example: Testing this component:

<p>Hello world!</p>

@{
    Console.WriteLine("Rendering RenderCountDemo");
}

@code {

    [Parameter]
    public int X { get; set; }

    protected override void OnParametersSet()
    {
        base.OnParametersSet();
        StateHasChanged();
        StateHasChanged();
        StateHasChanged();
    }
}

With this test:

[Fact]
public void Should_have_correct_RenderCount()
{
    var cut = Render<RenderCountDemo>(@<RenderCountDemo X="1" />);
    cut.SetParametersAndRender(); // This should only trigger one additional render.
    cut.RenderCount.Should().Be(2);
}

Results in this output:

Expected cut.RenderCount to be 2, but found 5

Expected behavior:

The test passes. Using the same component in a Blazor WASM app results in one render each time the parameter changes, but in BUnit I get three (edit: four). This is not just a problem with the “RenderCount” property, the actual render method is being called four times as well (confirmed with a breakpoint).

Version info:

  • bUnit version: 1.20.8
  • .NET Runtime and Blazor version: .NET 6.0
  • OS type and version: Windows 10

Additional context:

I am obviously not calling StateHasChanged() multiple times in succession like the example above, but there are lots of dependencies etc. which can result in redundant calls to StateHasChanged. In a normal Blazor app, the calls would get “batched” into a single render, so long as they occur synchronously.

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 25 (14 by maintainers)

Most upvoted comments

Thanks @egil, I tried out the latest preview and it’s working perfectly, including on my real tests.

Somewhat related: https://github.com/dotnet/aspnetcore/issues/48980 I opened a ticket for our v2.

Nice one. I’d argue that the majority of that code should live inside the TestRenderer itself - that would also remove the need of some of the reflection.

Neat find. I’ll play a little with reflection tonight and see what’s possible.

For V2 and .net 8 the ComponentState has been - made public so we may have a less hacky way to the goal line

@linkdotnet I’m not too familiar with the internals, but shouldn’t there ideally still be some straightforward way to update the parameters of a CUT, that represents what would happen in a real app? Making a separate parent component per test is doable, but still a lot more work than having an interface like SetParametersAndRender().

Whether it’s worth the time to implement is another question, of course.

Fair point - my argument goes in the direction that the render count is an internal thing. As such, it can vary depending on what you are doing.

To your original request, what you observed in the browser and what you are doing in the test - they are just different things. And yes it is a bit unfortunate that it isn’t visible to you. Functions like cut.SetParametersAndRender just don’t have a real pendant in the browser hence the different behavior and are meant as a convenient way.

Okay - here the reason for the different behavior between an initial render and a re-render. The initial render is triggered in our TestRenderer. the Blazor Renderer has the following line:

if (!_isBatchInProgress)
{
    ProcessPendingRender();
}

In AddToRenderQueue. In the initial case _isBatchInProgress is true - therefore all StateHasChanged calls are packed together and only one render cycle happens.

In the second case (i.e. cut.Render() or cut.SetParameteresAndRender()). _isBatchInProgress is false as ProcessRenderQueue is never called. We are calling this directly:

var result = renderedComponent.InvokeAsync(() =>
			renderedComponent.Instance.SetParametersAsync(parameters));

This bypasses ProcessRenderQueue completely. RenderComponent does not do that.

Pushed a branch with my (failing tests) for this issue to https://github.com/bUnit-dev/bUnit/tree/bug/1119-Components-render-too-many-times-with-SetParametersAndRender if somebody wants to take a look. Otherwise, I will see what I can do later during the summer.

Interesting - will have a look in the upcoming days.

Thanks @egil, I think I’ll be able to get by with your workaround (or something similar) for now. I’ll update this thread if I find anything useful. Here’s another workaround that seems to work:

@* RenderCountWrapper.razor *@
@ChildContent
@code {
    [Parameter] public RenderFragment? ChildContent { get; set; }
}
[Fact]
public void Should_have_correct_RenderCount() // passes
{
    var x = 10;
    var cut = Render<RenderCountWrapper>(@<RenderCountWrapper><RenderCountDemo X="x" /></RenderCountWrapper>);
    var component = cut.FindComponent<RenderCountDemo>();
    component.MarkupMatches("X=10");
    x = 20;
    cut.Render();
    component.RenderCount.Should().Be(2);
    component.MarkupMatches("X=20");
}