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)
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 theMarkupMatches
to be using entirely independent renderers, though.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 doubleConfigureAwait(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.
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…