-
-
Notifications
You must be signed in to change notification settings - Fork 107
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
Waiting operations and multi-threading #31
Comments
My comments on Open question 1 Advantages of blocking operations
Disadvantages
|
My comments on Open question 2 Advantages of running in single Razor Dispatcher context
Advantages of running tests in 2 synchronization contexts
|
Thanks for this @duracellko. A few points I would like us to consider in this discussion is:
As for your two pros/cons for each of the questions, I generally agree, but I also think we need to actually built a async version of the library to really be able to compare and contrast, have performance measurements, additional tests, etc.. Otherwise it's an (informed) guessing game. By the way, I would like to throw another discussion point into the mix. Currently, the "Event Dispatch Extensions" all call public static void Click(this IElement element, MouseEventArgs eventArgs)
=> _ = ClickAsync(element, eventArgs); I.e. the task is not being awaited, just discarded. Why does that work? Why does the renderer always complete rendering after an unwaited Click call? e.g. like in this example: https://github.com/egil/razor-components-testing-library/blob/master/sample/tests/Tests/Pages/CounterTest.cs#L34-L53 |
After having better look at Tasks are enqueued only, when there is another task running at the moment. That means that in most of test scenarios everything would run on single thread. |
Interesting. Good find. So it is likely why we have the Is my understanding correct then that when |
Yes it is very likely. |
So @duracellko do you think it is worth the effort to investigate this further for now? maybe built a prototype and see how it behaves? |
Also, I am wondering if we should be using |
I don't know what is advantage of |
Well, before investigating I think it's important to define goals and principles. For example, we can invest into prototype to verify performance, if it is the main goal. Although I don't think high performance should be the main goal of testing framework. Also after finding out how RendererSynchronizationContext schedules work, there will be not much difference between having single synchronization context or two separate ones. However, the question is, how much should the framework follow Inversion of Control pattern. That's the pattern that async/await and And of course with more control comes more responsibility. Therefore implementation of client can get more complicated. However, thanks to async/await in C# this complexity can be usually very well hidden. So main question is how much control does the test framework should give to client. And this is not related only to task awaiting. For example we had also discussion about |
Also I can create a prototype of using the test framework with some integration tests. For example with not mocking HttpClient, but doing real HTTP requests. I think WaitForNextRender is not the best API in such case. Because it may be unpredictable if the next render already happened or not. |
I agree. Lets take performance out of the equation for now.
I agree. Although I like that we have two, since, as you said previously, that mimics the way production code works.
Great observations. In general, I do not mind giving responsibility to the users, if they get a benefit from it. Otherwise I prefer to keep things simple and require as little typing as possible from the user.
You are correct, the
So that might be an actual good reason to go async. If we have a |
That would be an interesting scenario to verify. If you do, the integration tests that perform real http calls should probably be in its own testing project, and ideally, the http calls it performs could be to itself or some localhost api, such that there will never be an issue running the integration testing lib without an internet connection. |
@duracellko just wanted you to know that I've started #5 and with that I'm experimenting with having the core renderers be async by default. Then the test contexts can choose whether to expose the async api or not. |
I created a prototype with integration testing. It introduces WaitForRenderAsync method that takes predicate and waits until the component is rendered so that the predicate is true. |
Great. It will be interesting to see what use cases it solves. I am currently working on incorporating the E2E tests from the aspnetcore repo, to get a lot more rendering cases covered. One of the first causes trouble with the current beta 5.1. It might be of interest for you to test against as well. Its the last Test: [Fact]
public void CanTriggerAsyncEventHandlers()
{
// Initial state is stopped
var cut = RenderComponent<AsyncEventHandlerComponent>();
var stateElement = cut.Find("#state");
Assert.Equal("Stopped", stateElement.TextContent);
// Clicking 'tick' changes the state, and starts a task
cut.Find("#tick").Click();
Assert.Equal("Started", cut.Find("#state").TextContent);
// Clicking 'tock' completes the task, which updates the state
// This click causes two renders, thus something is needed to await here.
cut.Find("#tock").Click();
Assert.Equal("Stopped", cut.Find("#state").TextContent);
} And the component under test: @using System.Threading.Tasks
<div>
<span id="state">@state</span>
<button id="tick" @onclick="Tick">Tick</button>
<button id="tock" @onclick="Tock">Tock</button>
</div>
@code
{
TaskCompletionSource<object> _tcs;
string state = "Stopped";
Task Tick(MouseEventArgs e)
{
if (_tcs == null)
{
_tcs = new TaskCompletionSource<object>();
state = "Started";
return _tcs.Task.ContinueWith((task) =>
{
state = "Stopped";
_tcs = null;
});
}
return Task.CompletedTask;
}
void Tock(MouseEventArgs e)
{
_tcs.TrySetResult(null);
}
} |
Actually this may be solved by In unit-tests it is fine to assume, when render occurs. Actually it may be even goal of the test to verify that. It is because there should not be any side-effects, when component is isolated. Therefore it is fine for a test to expect 1 or 2 renders and then verify the rendering result. However, with integration tests it's not that simple. The component is not isolated and tests should not test, when rendering occurs. Only if after a specified time there is expected result rendered. Selenium in End-2-End tests solves that by making Query methods more resilient. If the requested element is not found, it tries again until timeout elapses. WaitForRenderAsync does same thing. And it is more flexible. Because query can be function. And also bit more reliable, because it can connect directly to rendering "events" of ASP.NET Core components. Btw. the integration tests in the example are inspired by https://github.com/duracellko/planningpoker4azure, which are inspired by End-2-End in ASP.NET Core Components, you just mentioned. |
Interesting. I'll have a closer look later. |
Ive pushed the WIP branch with the That will also close the #29 issue. |
@using System.Threading.Tasks
<div>
<span id="state">@state</span>
<button id="tick" @onclick="Tick">Tick</button>
<button id="tock" @onclick="Tock" disabled="@(!tockEnabled)">Tock</button>
</div>
@code
{
bool tockEnabled = false;
TaskCompletionSource<object> _tcs;
string state = "Stopped";
Task Tick(MouseEventArgs e)
{
if (_tcs == null)
{
_tcs = new TaskCompletionSource<object>();
state = "Started";
tockEnabled = true;
return _tcs.Task.ContinueWith((task) =>
{
state = "Stopped";
_tcs = null;
});
}
return Task.CompletedTask;
}
void Tock(MouseEventArgs e)
{
tockEnabled = false;
_tcs.TrySetResult(null);
}
} In this case Tock button is enabled only, when started. And especially in this case, there are not only 2 renders, but also 2 changes. |
Great test case @duracellko. We definitely need more of these - out of the ordinary - test scenarios covered. I like your idea about using a predicate, it is a more general and flexible way of blocking a test. But let me suggestion an extension to that idea.
Here is some pseudo code for it (written in GitHub, not tested at all): public static T ShouldPass(this T cut, Action<T> verificationAction, TimeSpan? timeout = null) where T : IRenderedFragment
{
TimeSpan timeLeft = Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout ?? TimeSpan.FromSeconds(1);
Exception? verificationFailure;
TryVerification();
while(verificationFailure is { } && timeLeft > TimeSpan.Zero)
{
// Add StopWatch logic to record time spend waiting
cut.NextChange.Wait(timeLeft);
timeLeft = timeLeft - time spend waiting for NextChange;
if(timeLeft > TimeSpan.Zero)
{
TryVerification();
}
}
if(verificationFailure is {})
{
ExceptionDispatchInfo.Capture(verificationFailure ).Throw();
}
return cut;
void TryVerification()
{
try { verificationAction(cut); verificationFailure = null; }
catch(Exception e) { verificationFailure = e; }
}
} I think that will lead to a very neat assert pattern, that will complete as soon as possible, e.g.: [Fact]
public void CanTriggerAsyncEventHandlers()
{
// Initial state is stopped
var cut = RenderComponent<AsyncEventHandlerComponent>();
var stateElement = cut.Find("#state");
Assert.Equal("Stopped", stateElement.TextContent);
// Clicking 'tick' changes the state, and starts a task
cut.Find("#tick").Click();
Assert.Equal("Started", cut.Find("#state").TextContent);
// Clicking 'tock' completes the task, which updates the state
// This click causes two renders, thus something is needed to await here.
cut.Find("#tock").Click();
cut.ShouldPass(cut => Assert.Equal("Stopped", cut.Find("#state").TextContent);
// or
cut.ShouldPass(cut => Assert.Equal("Stopped", cut.Find("#state").TextContent), TimeSpan.FromMilliSecond(200));
} I've never been a fan of the The trick will be to get the naming right, which I am not sure it is now :) What do you think. UPDATE: The name should probably have something in it that indicates it is meant for dealing with asynchronous rendering scenarios. |
Good idea. I will try to create PR for that. I will think about the naming. Regarding point 2: I would be careful about exposing I can imagine For this reason Actually |
I do not think it will be a problem but please validate this. Just to be sure we are talking about the same thing, the code would be something like this: private TaskCompletetionSource<object?> _nextRender;
private TaskCompletetionSource<object?> _nextChange;
public Task NextRender => _nextRender.Task;
public Task NextChange => _nextChange.Task; So the
I agree. I think
Dont you mean "For this reason NextChange should be preferred in unit-tests."? Or did I misunderstand you?
Indeed. And I think we have an advantage here, at least over Selenium , in that we know when a change has happened, so there is less potential wait time. About NextRender/NextChange and ShouldPass, lets move that discussion to the issue tracking it: #29 |
Yes, I understand. I am just not sure, if the code should be like this: private volatile TaskCompletetionSource<object?> _nextRender;
private volatile TaskCompletetionSource<object?> _nextChange;
public Task NextRender => _nextRender.Task;
public Task NextChange => _nextChange.Task;
I meant NextRender. Example: Component: <!-- This is bug. It should be bound to @counter -->
<p>@initialValue</p>
<button @onclick="Counter">Counter</button>
@code
{
private readonly int initialValue = 1;
private int counter;
protected override void OnInitialized()
{
base.OnInitialized();
counter = initialValue;
}
private void Counter()
{
counter++;
}
} Tests: public void Test01()
{
var cut = RenderComponent<MyComponent>();
cut.WaitForNextRender(() => cut.Find("button").Click());
cut.Find("p").TextContent.ShouldBe("2");
}
public void Test02()
{
var cut = RenderComponent<MyComponent>();
cut.WaitForNextChange(() => cut.Find("button").Click());
cut.Find("p").TextContent.ShouldBe("2");
} I like Test01 more than Test02. Test01 fails immediately (few ms) and with explaining error: "Expected value was '2', but actual value was '1'." Test02 fails after timeout (1 second) and with error: "There was no change in time out." And that is not very good point to start investigation. |
Ill close this, as it seems it has served its purpose. |
This issue tracks discussion about architectural decisions about asynchronous operations and multi-threading.
Current status
This discussion was initiated by identification of following points during PR #27:
WaitForNextRender
andDispatchAndAssertNoSynchronousErrors
are blocking and not returning aTask
object.Open question 1
Shall in general waiting operations like
WaitForNextRender
andDispatchAndAssertNoSynchronousErrors
be blocking? Or should they return Task object that can be awaited?Open question 2
Should whole test run in synchronization context of Razor Dispatcher? Or is it desired to run tests in 2 synchronization contexts?
The text was updated successfully, but these errors were encountered: