-
-
Notifications
You must be signed in to change notification settings - Fork 113
Make WaitForNextRender method asynchronous #27
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
Conversation
duracellko
commented
Jan 5, 2020
- Make WaitForNextRender asynchronous, instead of blocking until NextRender Task is completed. Blocking on task may lead to deadlocks.
- Add support for asynchronous tests in Fixture so that WaitForNextRender can be called properly.
Hey thanks for this @duracellko. It looks like good additions. I will take good look at this tomorrow and get back to you. |
Hi @duracellko, thanks again for this. I have some feedback, that I hope you have time to incorporate into the PR. If not, I will do it later this week (hopefully).
Thanks again, let me know what you think. |
Hello, thank you for your feedback.
Task MyTest()
{
// do test
return Task.CompletedTask;
}
|
Awesome. Appreciate it.
Another thing. Naming is hard. I am not really a fan of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See previous comment for suggested changes.
FYI: I have added an issue that is somewhat related (#29). However, I do not think it should be included in this PR. Lets focus on figuring this out first, then we/I can take a look at that issue. |
I created |
tests/TestContextTest.cs
Outdated
|
||
namespace Egil.RazorComponents.Testing | ||
{ | ||
public class TestContextTest |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
public class TestContextTest | |
public class TestContextTest : ComponentTestFixture |
By inherting from ComponentTestFixture
, we get a TextContext implicitly, follow the convention from the other tests that also does this, and do not have to deal with disposing of the context after each test. It is handled by the fixture.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I don't like this.
- I don't think the test framework should be used to test itself. Or at least tests of
TestContext
should not depend onComponentTestFixture
that depends onTestContext
. - Inheriting
ComponentTestFixture
fromTestContext
can lead to flaky tests (or tests depending on order). For example in one of my tests I setup Services. And this setup of services is then carried on to other tests. But it depends on order of execution to which tests.
In general test classes should be stateless and new state should be constructed in each test. So ComponentTestFixture
is anti-pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Order of tests does not matter at all since xUnit creates a new instance of the TestContextTest
for each test/fact it executes, and when the test is done, it runs the dispose method inherited by TestContextTest
.
That means every test has it's own test context.
As to the first point, I get what you are saying. But since ComponentTestFixture
just provides helper methods for tests, and it is a test context, I do think it makes sense to do it like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yes. I forgot that xUnit works differently than NUnit and MSTest. I will update the pattern then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First of, this is very solid code. Thank you for the effort.
My comments and suggestions for changes are more directed at code style and my wish to keep things consistent in the library.
A question. Were you able to find an example of a combination of calls that actually causes a deadlock if WaitForNextRender
is not async? If so I would very much like to see it. I am trying to understand the nature of this problem, and if this problem also exists with the TestRenderer
which basically blocks everytime it renders in its DispatchAndAssertNoSynchronousErrors
method (has a call to .Wait()
).
Another thought: If we change WaitForNextRender
to the async form you are proposing, I am tempted to rename it to NextRenderAfter(Action? renderTrigger, TimeSpan? timeout)
. It makes it more clear to the user that what they are awaiting is the next render after whatever action has completed they provide is invoked. What do you think?
tests/TestContextTest.cs
Outdated
{ | ||
public class TestContextTest | ||
{ | ||
[Fact(DisplayName = "Constructor setup default values")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure this test has any benefit. What it verifies is tested implicitly by all the other tests. As such I think it can be safely deleted without loosing test coverage.
tests/TestContextTest.cs
Outdated
[Fact(DisplayName = "Wait for first render")] | ||
public async Task Test002() | ||
{ | ||
using (var target = CreateTestContext()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
using (var target = CreateTestContext()) |
Now that the test classes is a Test Context through inheritance, this is not needed
tests/TestContextTest.cs
Outdated
public async Task Test002() | ||
{ | ||
using (var target = CreateTestContext()) | ||
{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{ |
tests/TestContextTest.cs
Outdated
{ | ||
using (var target = CreateTestContext()) | ||
{ | ||
var renderTask = target.WaitForNextRender(NoOperation); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var renderTask = target.WaitForNextRender(NoOperation); | |
var renderTask = WaitForNextRender(NoOperation); |
tests/TestContextTest.cs
Outdated
var renderTask = target.WaitForNextRender(NoOperation); | ||
renderTask.IsCompleted.ShouldBeFalse(); | ||
|
||
target.RenderComponent<Simple1>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
target.RenderComponent<Simple1>(); | |
RenderComponent<Simple1>(); |
tests/TestContextTest.cs
Outdated
var button = cut.Find("button"); | ||
var renderTask = target.WaitForNextRender(() => button.Click()); | ||
|
||
// Render is done on button click | ||
asyncTestDepMock.Verify(o => o.GetData()); | ||
renderTask.IsCompleted.ShouldBeTrue(); | ||
await renderTask; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This part of the test is not entirely clear to me. button.Click()
is synchronous and blocks until the render is done, so is this part relevant?
Could this be replaced by
var button = cut.Find("button"); | |
var renderTask = target.WaitForNextRender(() => button.Click()); | |
// Render is done on button click | |
asyncTestDepMock.Verify(o => o.GetData()); | |
renderTask.IsCompleted.ShouldBeTrue(); | |
await renderTask; | |
cut.Find("button").Click(); |
tests/TestContextTest.cs
Outdated
} | ||
} | ||
|
||
[Fact(DisplayName = "No render when asynchronous event fails")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test should be updated to remove the using(var target and related code, as the previous test.
tests/TestContextTest.cs
Outdated
var button = cut.Find("button"); | ||
var renderTask = target.WaitForNextRender(() => button.Click()); | ||
|
||
// Render is done on button click | ||
asyncTestDepMock.Verify(o => o.GetData()); | ||
renderTask.IsCompleted.ShouldBeTrue(); | ||
await renderTask; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same comment as with the previous test.
tests/TestContextTest.cs
Outdated
private static TestContext CreateTestContext() | ||
{ | ||
return new TestContext(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be deleted.
tests/TestContextTest.cs
Outdated
private static void NoOperation() | ||
{ | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fact that you need a method like this in this particular test scenario makes me think that maybe the the signature for the WaitForNextRender
should be WaitForNextRender(Action? renderTrigger, TimeSpan? timeout)
instead, i.e. making renderTrigger optional. If none is passed in, we still do the heavy lifting of setting up a task that times out after a specific TimeSpan or a render completes.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that renderTrigger
could be optional. Now the question is, what is preference?
Method overload:
public Task WaitForNextRender(TimeSpan? timeout = null) => WaitForNextRender(null, timeout);
public async Task WaitForNextRender(Action? renderTrigger, TimeSpan? timeout = null)
{
...
}
Default parameter:
public async Task WaitForNextRender(Action? renderTrigger = null, TimeSpan? timeout = null)
{
...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I prefer the default parameter variant. In the end the compiler turns it into the same, so why waste keystrokes :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will update it.
FYI: It's not compiled exactly the same.
Overload is compiled in provider DLL. For example there are libraries A.dll references B.dll. B.dll provides methods void M(string val); void M() => M(null);
And A.dll has code M();
. Then later B.dll is updated to version 2 with M(string val); M() => M(string.Empty);
. When B.dll is deployed, then result is that M(string.Empty)
is executed without recompiling A.dll. New default is used without recompiling DLL.
Default parameter is compiled into client DLL. For example there are libraries A.dll references B.dll. B.dll provides methods void M(string val = null);
And A.dll has code M();
. Then later B.dll is updated to version 2 with M(string val = string.Empty);
. When B.dll is deployed, then result is that M(null)
is executed without recompiling A.dll. New default is used only after recompiling DLL.
Same difference is between public readonly string Value = "V"
and public const string Value = "V"
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation. Didn't know that.
I added test "WaitForNextRender is not blocked in Dispatcher context". It ensures that it is not blocked. Then I updated test in this branch https://github.com/duracellko/razor-components-testing-library/blob/WaitForNextRender-blocked/tests/TestContextTest.cs. And this test gets blocked. I didn't realize that tests do not run in the same synchronization context as the Dispatcher by default. So deadlock happens, only when test explicitly runs on Dispatcher. Also in general it's better to have non-blocking task. As you can see in other tests, it's easier to test, that the task is not finished. |
Yes, I noticed
I think it looks good. I will thing about the name. |
Just a quick comment regarding the general use of blocking logic (I'll be back with more later). I do want to keep the api clean for the users. And if they have to write The code related to the dispatcher and WaitForNextRender is actually taken more or less verbatim from Steve Sanderson prototype (https://github.com/SteveSandersonMS/BlazorUnitTestingPrototype). Since each test runs in its own xunit sync contexts, and each test also has it's own renderer with it's dispatcher with sync context, we might not have deadlock issues at all. |
OK, so I've been reading up on Synchronization Contexts and have a better understanding now, thanks for the push to do so :-)
In conclusion, I really appreciate your efforts with this, but I think we should take a step back and reevaluate the changes to I do think it is useful to have Ps. this modified version of your deadlocking tests now just throws a TimeoutException: [Fact(DisplayName = "WaitForNextRender is not blocked in Dispatcher context")]
public async Task Test008()
{
using (var waitHandle = new ManualResetEvent(false))
{
string GetTestData()
{
var result = waitHandle.WaitOne(TimeSpan.FromSeconds(1));
Thread.Sleep(100);
return result ? "Test" : string.Empty;
}
var asyncTestDepMock = new Mock<IAsyncTestDep>();
asyncTestDepMock.Setup(o => o.GetData()).Returns(Task.Run(GetTestData));
Services.AddService<IAsyncTestDep>(asyncTestDepMock.Object);
var cut = RenderComponent<SimpleWithAyncDeps>();
await Renderer.Dispatcher.InvokeAsync(() =>
{
WaitForNextRender(() => waitHandle.Set());
});
cut.Find("p").TextContent.ShouldBe("Test");
}
} |
Yes. Please do. Let's have a look at the async/sync code and see where we can make changes to benefit the users and the performance of the library. |
Ok, I will rewrite the branch just with changes on |
Awesome. Appreciate it! |
Co-Authored-By: Egil Hansen <[email protected]>
Sorry, for late response. Last few days were busy. I updated the branch and created issue #31. |
No worries at all. This is not the primary occupation for none of us. I will review and merge on Wednesday. Busy preparing for .net conf tomorrow. |
* Fixed spelling mistake in comment * Reordered parameters to top of TestRenderer * Add support for asynchronous Razor tests (#27) * Add support for asyncrhonous Razor tests * Rename Fixture.AsyncTest to TestAsync * Update description of Fixture Co-Authored-By: Egil Hansen <[email protected]> * Dotnetconf samples (#33) * Basic example tests * Add missing MarkupMatches overload * Added docs to MockHttp extensions * dotnet conf samples * Added unit tests of CompareTo, Generic and Collection assert extensions. * Added tests for general events and touch events dispatch extensions * Added tests for anglesharp extensions, JsRuntimeInvocation and ComponentParameter * Removed assert helpers that conflict with Shoudly * Tests of JsRuntimeAsserts * Reorganized test library * Suppressing warnings in sample * Changed ITestContext to have a CreateNodes method instead of HtmlParser property * Removed empty test * Added missing code documentation * Moved MockJsRuntime to its own namespace * Pulled sample from main solution into own solution * Update main.yml * Change GetNodes and GetMarkup to Nodes and Markup properties in IRenderedFragment (#34) * Add SetupVoid and SetVoidResult capabilities to JsRuntime mock (#35) * Moved MockJsRuntime to its own namespace * Add SetupVoid() and SetVoidResult() to PlannedInvocation and Mock * Updates to template and sample * Add default JsRuntime (#32) * Add default JsRuntime * Update src/Mocking/JSInterop/DefaultJsRuntime.cs Co-Authored-By: Egil Hansen <[email protected]> * Response to review. Add custom exception and code cleanup * Remove unneded method * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> * Update src/Mocking/JSInterop/MissingMockJsRuntimeException.cs Co-Authored-By: Egil Hansen <[email protected]> Co-authored-by: Egil Hansen <[email protected]> * Add .vscode to gitignore * TestServiceProvider now explictly implements IServiceCOlelction (#40) * TestServiceProvider now explictly implements IServiceCOlelction * Tweaks to namespaces * Added async setup method to snapshot test * Added test of SnapshotTests use of setup methods * Update to readme * Removed duplicated MarkupMatches method * Removed PR trigger Co-authored-by: Rastislav Novotný <[email protected]> Co-authored-by: Michael J Conrad <[email protected]>