bUnit: FragmentContainer was not found in async test

Describe the bug

I have a relatively simple component that just renders different content depending on the state of a Task. The code of both component and test is very similar to the async example except that instead of awaiting the task on init I’m using task.ContinueWith(...) => InvokeAsync(StateHasChanged) and using the razor test syntax.

When using bUnit 1.16.2 and not using WaitForAssertion, the tests almost always pass. (I did very very rarely observe the same waiting failure as below.) When using later versions of bUnit, the tests will more frequently intermittently fail (showing the waiting content rather than the done content). When I tried adding WaitForAssertion (in 1.16.2) it started instead failing with:

Bunit.Extensions.WaitForHelpers.WaitForFailedException : The assertion did not pass within the timeout period. Check count: 2. Component render count: 2. Total render count: 5.
  ----> Bunit.Rendering.ComponentNotFoundException : A component of type FragmentContainer was not found in the render tree.
   at Bunit.RenderedFragmentWaitForHelperExtensions.WaitForAssertion(IRenderedFragmentBase renderedFragment, Action assertion, Nullable`1 timeout) in /_/src/bunit.core/Extensions/WaitForHelpers/RenderedFragmentWaitForHelperExtensions.cs:line 72

I haven’t been able to replicate precisely this behaviour in a MCVE test, but what I did manage to reproduce is described below.

(Oddly, the MCVE code always fails (with waiting content, not the exception) when not using WaitForAssertion. While not exactly surprising due to async, it’s odd that it’s different; though it’s likely that this is due to the real component being a bit more complex.)

Example: Testing this component:

@if (Task != null)
{
    @if (Task.IsCompleted)
    {
        <span>done</span>
    }
    else
    {
        <span>waiting</span>
    }
}

@code {

    [Parameter] public Task? Task { get; set; }

    private Task? _RegisteredTask;

    protected override void OnParametersSet()
    {
        var task = Task;
        if (task != _RegisteredTask)
        {
            _RegisteredTask = task;

            _ = task?.ContinueWith((t, o) =>
            {
                if (t == Task)
                {
                    _ = InvokeAsync(StateHasChanged);
                }
            }, null);
        }

        base.OnParametersSet();
    }

}

With this test:

@using NUnit.Framework
@using Bunit
@*@inherits Bunit.TestContext*@
@inherits BunitTestContext  /* this uses TestContextWrapper */
@code {

    [Test]
    public void Cancel1()
    {
        var tcs = new TaskCompletionSource();

        using var cut = Render(@<MyComponent Task="@tcs.Task"/>);

        cut.MarkupMatches(@<span>waiting</span>);

        tcs.SetCanceled();

        cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
    }

    [Test]
    public void Cancel2()
    {
        var tcs = new TaskCompletionSource();

        using var cut = Render(@<MyComponent Task="@tcs.Task"/>);

        cut.MarkupMatches(@<span>waiting</span>);

        tcs.SetCanceled();

        cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
    }

    [Test]
    public void Cancel3()
    {
        var tcs = new TaskCompletionSource();

        using var cut = Render(@<MyComponent Task="@tcs.Task"/>);

        cut.MarkupMatches(@<span>waiting</span>);

        tcs.SetCanceled();

        cut.WaitForAssertion(() => cut.MarkupMatches(@<span>done</span>));
    }

}

(Note that this is three identical copies of the same test.)

Expected behavior:

All tests should pass.

Actual behavior:

The first test always passes. The other two tests intermittently fail with the exception above.

If I run the tests in the debugger (without stopping on any breakpoints or exceptions), all tests pass.

Version info:

  • bUnit version: 1.21.9
  • Blazor version: 6.0.18
  • .NET Runtime version: 6.0.405 (SDK 7.0.102)
  • OS type and version: Windows 10, VS2022

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 40 (17 by maintainers)

Most upvoted comments

Yes, it’s the same FragmentContainer exception.

I try to find some time to provide a fix

It would definitely need to make services from the TestContext available to the markup renderer. While often you’re comparing against HTML primitives, sometimes you’re not – for example I have quite a few tests that compare a large component against smaller components that end up generating complex SVG internally (but I don’t want to write that level of detail into the test source, even if it internally compares at that level).

Granted, I don’t think I currently have any tests comparing against components that have complex service dependencies (and certainly not anything async), but it wouldn’t surprise me if someone does, even if only a logger.

It does make sense to me for the cut and the MarkupMatches to be using entirely independent renderers, though.

Suggestions?

We could instantiate our own renderer-instance for MarkupMatches that is completely detached from the cut.

That will likely work. Should we attempt to reuse renderer instances if they are not currently blocked?

What about Services? We register a renderer in there that is getting pulled out in certain circumstances, should that be a transient registration instead?

Here is all the code of the Render<TResult> that was partly linked above.

https://github.com/bUnit-dev/bUnit/blob/674a5559d1cc572de0bea9dbb203b6f7358316d0/src/bunit.core/Rendering/TestRenderer.cs#L349-L389

Even after the renderTask is completed there is still a chance that the root component has not rendered yet.

If you download the 1143-fragmentcontainer-not-found branch you will see that there are a few other tests besides the Cancel one that are also failing due to the InvalidOperationException being thrown.

FWIW after I changed my real app to use the latest MCVE version (OnComplete with double ConfigureAwait(false)), while I’ve never managed to get it to fail again on my machine, it still happens occasionally on a slower machine. But it’s a lot rarer than previously.

That is my guess too. In addition, I guess that the render of the markup matches fragment actually isn’t able to run (renderer is locked) because the async triggered render by the TCS is blocking the renderer because it is rendering.

1.21.9 is the latest release, isn’t it? At least it’s the newest on nuget.org…

Did the latest release fix the issue?

No, I think you misunderstood. This was the latest release the whole time, at least for the MCVE (see the bottom of the original post).

1.21.9 is the latest release, isn’t it? At least it’s the newest on nuget.org…