Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to wait for SetParametersAsync in test? #157

Closed
JeroenBos opened this issue Jun 22, 2020 · 3 comments
Closed

How to wait for SetParametersAsync in test? #157

JeroenBos opened this issue Jun 22, 2020 · 3 comments
Labels
question Further information is requested

Comments

@JeroenBos
Copy link
Contributor

JeroenBos commented Jun 22, 2020

Description of testing scenario:
What I'm trying to achieve is to compare the rendered html of a component whose SetParameterAsync performs long-running work. The comparison test is supposed to take place after that work has taken place, and consistently after. Preferably not by inserting Task.Delay with an arbitrary number of seconds.

I have something like the following in mind (as written, the test fails):

ComponentWithWorkInSetParameterAsync.razor:

@delayedWork

@code {
    private string delayedWork = "Loading";

    public override async Task SetParametersAsync(ParameterView parameters)
    {
        await base.SetParametersAsync(parameters);
        await Task.Delay(1000); // simulate some work
        delayedWork = "Done";
        this.StateHasChanged();
    }
}
@inherits TestComponentBase

<SnapshotTest>
  <TestInput>
      <ComponentWithWorkInSetParameterAsync/>
  </TestInput>
  <ExpectedOutput>Done</ExpectedOutput>
</SnapshotTest>

The test fails with the obvious error:

Actual HTML: Loading
Expected HTML: Done

Attempted solution

Now, the tests runs to success if you placed Task.Delay(2000); in Bunit.SnapshotTest.Run between these two lines, like so:

var testRenderId = Renderer.RenderFragment(TestInput);
Task.Delay(2000);
var inputHtml = Htmlizer.GetHtml(Renderer, testRenderId);

This is sort of the approach that I was taking (by constructing a MySnapshotTest, if you will).
Now I have a very specific question:

What would you write instead of Task.Delay(2000); that waits on the SetParameterAsync?


I was hoping to find members like Renderer.WaitAll() or Renderer.Dispatcher.WaitAll() or something but I can't seem to find it. Using reflection, I tried waiting for the private list Microsoft.AspNetCore.Components.RenderTree.Renderer._pendingTasks as well, doesn't work either.


Where does the task returned by SetParametersAsync go, anyway?

I followed it by debugging the blazor source code and I think it gets discarded? In particular here:
https://source.dot.net/#Microsoft.AspNetCore.Components/RenderTree/Renderer.cs,300
because _pendingTasks is null. The call is coming from
https://source.dot.net/#Microsoft.AspNetCore.Components/Rendering/ComponentState.cs,157

To be precise, I followed it when var testRenderId = Renderer.RenderFragment(TestInput); was replaced by var (testRenderId, cut) = this.Renderer.RenderComponent<ComponentWithWorkInSetParameterAsync>(Array.Empty<ComponentParameter>()); but I don't think it matters.

Additional context:
dotnet=3.1.300. bUnit version master today.

Conclusion

It seems the Task that I would like to wait for is unobtainable. Although I hope that I can be shown wrong?

@JeroenBos JeroenBos added the question Further information is requested label Jun 22, 2020
@egil
Copy link
Member

egil commented Jun 23, 2020

Hey @JeroenBos, thanks for a very well formed question, makes it very easy to help you.

The <SnapshotTest> is a little limited right now, but I have been thinking of adding a WaitForState parameter to it, which takes a predicate that must return true before continuing.

Until then, you can write your test like this:

@inherits TestComponentBase

<Fixture Test="OutputMatchesFragment">
  <ComponentUnderTest>
    <ComponentWithWorkInSetParameterAsync />
  </ComponentUnderTest>
  <Fragment>Done</Fragment>
  @code
  {
    public void OutputMatchesFragment(Fixture fixture)
    {
      var cut = fixture.GetComponentUnderTest();
      var expected = fixture.GetFragment();
      cut.WaitForAssertion(() => cut.MarkupMatches(expected), TimeSpan.FromSeconds(2));
    }
  }
</Fixture>

The WaitForAssertion method is the trick to making this work. It will attempt the assertion action passed to it until the timeout is reached. As soon as the assertion passes, it returns. The timeout is optional and defaults to one second if not specified.

The MarkupMatches method is what <SnapshotTest> uses internally, so the code you see above is basically the same as using a snapshot test.

The background to this is described here, if you want to know the details of what is going on in the test: https://bunit.egilhansen.com/docs/interaction/awaiting-async-state.html

@JeroenBos
Copy link
Contributor Author

https://bunit.egilhansen.com/docs/interaction/awaiting-async-state.html

Thank you. Very useful link. Your proposed solution works. Pragmatically speaking, I will use it.


Principally, though it doesn't sit too right with me. The link mentions mentions the two synchronization contexts, framework and renderer. Wouldn't the "proper" way of waiting on the async work involve one synchronization context waiting for the other, rather than checking some delegate at times it could have changed? Let me suggest what I would like to usage to look like:

<Fixture Test="OutputMatchesFragment">
  <ComponentUnderTest>
    <ComponentWithWorkInSetParameterAsync />
  </ComponentUnderTest>
  <Fragment>Done</Fragment>
  @code
  {
    public async Task OutputMatchesFragment(Fixture fixture)
    {
      var cut = fixture.GetComponentUnderTest();
      var expected = fixture.GetFragment();  
      await cut.WaitForSetParametersAsync(); // and cut.WaitForOnInitializedAsync(), etc
      cut.MarkupMatches(expected);
    }
  }
</Fixture>

where WaitForSetParametersAsync() would wait for the task returned fromSetParametersAsync to complete.

I'm unsure of how WaitForSetParametersAsync would be implemented, whether it's even possible. But you see how that would make the test read as its intent?

That's would be very lovely functionality by bUnit, and also something I expected to be there. I guess this has turned this question into a feature request 😀

@egil
Copy link
Member

egil commented Jun 23, 2020

@JeroenBos there is actually an issue #124 trying to address this. The ideal solution would in my view be to have just one sync context, such that there is no need to wait at all. My concern is deadlocks, but I will have to investigate when I get to that issue, or somebody does it for me (hint hint).

As for your proposal, I do not think it is possible, or at least not without reimplementing the entire rendering pipeline in Blazor.

I'll take the liberty to close this issue. Let me know if you have any more questions.

@egil egil closed this as completed Jun 23, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants