Triggering a render life cycle on a component
To trigger a re-render of a component under test, a reference to it through a IRenderedComponent<TComponent> type is needed. When using the TestContext's RenderComponent<TComponent>()
method, this is the type returned.
In .razor
based tests, using the TestContext's Render(RenderFragment) method also returns an IRenderedComponent<TComponent> (as opposed to the Render(RenderFragment) method which returns the more simple IRenderedFragment).
If you have a IRenderedFragment or a IRenderedComponent<TComponent> in a test, but need a child component's IRenderedComponent<TComponent>, then use the FindComponent<TComponent>()
or the FindComponents<TComponent>()
methods, which traverse down the render tree and finds rendered components.
With a IRenderedComponent<TComponent>, it is possible to cause the component to render again directly through the Render()
method or one of the SetParametersAndRender()
methods, or indirectly through the InvokeAsync()
method.
Let's look at how to use each of these methods to cause a re-render.
Render
The Render()
method tells the renderer to re-render the component, i.e. go through its life-cycle methods (except for OnInitialized()
and OnInitializedAsync()
methods). To use it, do the following:
var cut = RenderComponent<Heading>();
Assert.Equal(1, cut.RenderCount);
// Re-render without new parameters
cut.Render();
Assert.Equal(2, cut.RenderCount);
The highlighted line shows the call to Render()
.
Tip
The number of renders a component has been through can be inspected and verified using the RenderCount property.
SetParametersAndRender
The SetParametersAndRender(...)
methods tells the renderer to re-render the component with new parameters, i.e. go through its life-cycle methods (except for OnInitialized()
and OnInitializedAsync()
methods), passing the new parameters — but only the new parameters — to the SetParametersAsync()
method. To use it, do the following:
var cut = RenderComponent<Item>(parameters => parameters
.Add(p => p.Value, "Foo")
);
cut.MarkupMatches("<span>Foo</span>");
// Re-render with new parameters
cut.SetParametersAndRender(parameters => parameters
.Add(p => p.Value, "Bar")
);
cut.MarkupMatches("<span>Bar</span>");
The highlighted line shows the call to SetParametersAndRender()
, which is also available as a version that takes the zero or more component parameters, e.g. created through the component parameter factory helper methods, if you prefer that method of passing parameters.
Note
Passing parameters to components through the SetParametersAndRender(...)
methods is identical to doing it with the RenderComponent<TComponent>(...)
methods, described in detail on the Passing parameters to components page.
InvokeAsync
Invoking methods on a component under test, which causes a render, e.g. by calling StateHasChanged
, can result in the following error, if the caller is running on another thread than the renderer's thread:
The current thread is not associated with the Dispatcher. Use
InvokeAsync()
to switch execution to the Dispatcher when triggering rendering or component state.
If you receive this error, you need to invoke your method inside an Action
delegate passed to the InvokeAsync()
method.
Let’s look at an example of this, using the <Calc>
component listed below:
<output>@result</output>
@code
{
int result = 0;
public void Calculate(int x, int y)
{
result = x + y;
StateHasChanged();
}
}
To invoke the Calculate()
method on the component instance, do the following:
var cut = RenderComponent<Calc>();
// Indirectly re-renders through the call to StateHasChanged
// in the Calculate(x, y) method.
cut.InvokeAsync(() => cut.Instance.Calculate(1, 2));
cut.MarkupMatches("<output>3</output>");
The highlighted line shows the call to InvokeAsync()
, which is passed an Action
delegate that calls the Calculate
method.
Tip
The instance of a component under test is available through the Instance property.
Advanced use cases
In some scenarios, the method being invoked may also return a value, as demonstrated in the following example.
<output>@result</output>
@code
{
int result = 0;
public int Calculate(int x, int y)
{
result = x + y;
StateHasChanged();
return result;
}
}
Testing this scenario follows the same procedure as before, with the addition of using the return value from InvokeAsync()
:
var cut = RenderComponent<CalcWithReturnValue>();
// Indirectly re-renders and returns a value.
var result = await cut.InvokeAsync(() => cut.Instance.Calculate(1, 2));
Assert.Equal(3, result);
cut.MarkupMatches("<output>3</output>");
This can also be used to assert intermediate states during an asynchronous operation, like the example below:
<output>@(result is null ? "Loading" : result.ToString())</output>
@code
{
int? result = 0;
public async Task Calculate(int x, int y)
{
result = null;
StateHasChanged();
// Simulate an asynchronous operation, like fetching data.
await Task.Delay(500);
result = x + y;
StateHasChanged();
}
}
// Arrange - renders the CalcWithLoading component
var cut = RenderComponent<CalcWithLoading>();
// Indirectly re-renders and returns the task returned by Calculate().
// The explicit <Task> here is important, otherwise the call to Calculate()
// will be awaited automatically.
var task = await cut.InvokeAsync<Task>(() => cut.Instance.Calculate(1, 2));
cut.MarkupMatches("<output>Loading</output>");
// Wait for the task to complete.
await task;
cut.WaitForAssertion(() => cut.MarkupMatches("<output>3</output>"));