Skip to content

Commit bdca10e

Browse files
committed
updates to docs and samples
1 parent 51d2f7c commit bdca10e

File tree

3 files changed

+106
-49
lines changed

3 files changed

+106
-49
lines changed

docs/csharp-examples.md

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ All examples can be found in the [CodeOnlyTests](../sample/tests/CodeOnlyTests)
1212
5. [Testing components with EventCallback parameters](#testing-components-with-eventcallback-parameters)
1313
6. [Testing components with cascading-value parameters](#testing-components-with-cascading-value-parameters)
1414
7. [Testing components that use on IJsRuntime](#testing-components-that-use-on-ijsruntime)
15-
7.1 [Verifying element references](#verifying-element-references)
16-
8. [Testing components with injected dependencies]()
15+
7.1 [Verifying element references passed to InvokeAsync](#verifying-element-references-passed-to-invokeasync)
16+
8. [Testing components with injected dependencies](#testing-components-with-injected-dependencies)
1717

1818
## Creating new test classes
1919

@@ -55,7 +55,7 @@ The following unit-tests verifies that the [Counter.razor](../sample/src/Pages/C
5555
}
5656
```
5757

58-
The [CounterTest.cs](../sample/test/CodeOnlyTests/Pages/CounterTest.cs) looks like this:
58+
The [CounterTest.cs](../sample/tests/CodeOnlyTests/Pages/CounterTest.cs) looks like this:
5959

6060
```csharp
6161
public class CounterTest : ComponentTestFixture
@@ -157,7 +157,7 @@ The component under test will be the [Aside.razor](../sample/src/Components/Asid
157157
}
158158
```
159159

160-
The [AsideTest.cs](../sample/test/CodeOnlyTests/Components/AsideTest.cs) looks like this:
160+
The [AsideTest.cs](../sample/tests/CodeOnlyTests/Components/AsideTest.cs) looks like this:
161161

162162
```csharp
163163
public class AsideTest : ComponentTestFixture
@@ -329,7 +329,7 @@ To show how to pass an `EventCallback` to a component under test, we will use th
329329
}
330330
```
331331

332-
The relevant part of [ThemedButtonTest.cs](../sample/test/CodeOnlyTests/Components/ThemedButtonTest.cs) looks like this:
332+
The relevant part of [ThemedButtonTest.cs](../sample/tests/CodeOnlyTests/Components/ThemedButtonTest.cs) looks like this:
333333

334334
```csharp
335335
public class ThemedButtonTest : ComponentTestFixture
@@ -456,7 +456,7 @@ To help us test the Mock JSRuntime, we have the [WikiSearch.razor](../sample/src
456456
}
457457
```
458458

459-
The [WikiSearchTest.cs](../sample/test/CodeOnlyTests/Components/WikiSearchTest.cs) looks like this:
459+
The [WikiSearchTest.cs](../sample/tests/CodeOnlyTests/Components/WikiSearchTest.cs) looks like this:
460460

461461
```csharp
462462
public class WikiSearchTest : ComponentTestFixture
@@ -546,7 +546,7 @@ For example, consider the [FocussingInput.razor](../sample/src/Components/Focuss
546546
}
547547
```
548548

549-
The the [FocussingInputTest.cs](../sample/test/CodeOnlyTests/Components/FocussingInputTest.cs) looks like this:
549+
The the [FocussingInputTest.cs](../sample/tests/CodeOnlyTests/Components/FocussingInputTest.cs) looks like this:
550550

551551
```csharp
552552
public class FocussingInputTest : ComponentTestFixture
@@ -573,3 +573,77 @@ public class FocussingInputTest : ComponentTestFixture
573573
```
574574

575575
The last line verifies that there was a single argument to the invocation, and via the `ShouldBeElementReferenceTo` checks, that the `<input />` was indeed the referenced element.
576+
577+
## Testing components with injected dependencies
578+
579+
The demonstrate service injection, lets refactor the [FetchData.razor](../sample/src/Pages/FetchData.razor) component that comes with the default Razor app template, to make it more testable:
580+
581+
- Extract an interface from [WeatherForecastService](../sample/src/Data/WeatherForecastService.cs), name it [IWeatherForecastService](../sample/src/Data/IWeatherForecastService.cs), and have `FetchData` take a dependency on it.
582+
583+
- Extract the `<table>` inside the `else` branch in the [FetchData.razor](../sample/src/Pages/FetchData.razor) component into its own component. Lets name it [ForecastDataTable](../sample/src/Pages/FetchData.razor).
584+
585+
- In the [FetchData.razor](../sample/src/Pages/FetchData.razor), pass the variable `forecasts` to the [ForecastDataTable](../sample/src/Pages/FetchData.razor) component.
586+
587+
Now we just need a [MockForecastService.cs](../sample/tests/MockForecastService.cs). It looks like this:
588+
589+
```csharp
590+
internal class MockForecastService : IWeatherForecastService
591+
{
592+
public TaskCompletionSource<WeatherForecast[]> Task { get; } = new TaskCompletionSource<WeatherForecast[]>();
593+
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate) => Task.Task;
594+
}
595+
```
596+
597+
With the mock in place, we can write the [FetchDataTest.cs](../sample/tests/CodeOnlyTests/Pages/FetchDataTest.cs), which looks like this:
598+
599+
```csharp
600+
public class FetchDataTest : ComponentTestFixture
601+
{
602+
[Fact(DisplayName = "Fetch data component renders expected initial markup")]
603+
public void Test001()
604+
{
605+
// Arrange - add the mock forecast service
606+
Services.AddService<IWeatherForecastService, MockForecastService>();
607+
608+
// Act - render the FetchData component
609+
var cut = RenderComponent<FetchData>();
610+
611+
// Assert that it renders the initial loading message
612+
var initialExpectedHtml = @"<h1>Weather forecast</h1>
613+
<p>This component demonstrates fetching data from a service.</p>
614+
<p><em>Loading...</em></p>";
615+
cut.ShouldBe(initialExpectedHtml);
616+
}
617+
618+
[Fact(DisplayName = "After data loads it is displayed in a ForecastTable component")]
619+
public void Test002()
620+
{
621+
// Setup the mock forecast service
622+
var forecasts = new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Testy", TemperatureC = 42 } };
623+
var mockForecastService = new MockForecastService();
624+
Services.AddService<IWeatherForecastService>(mockForecastService);
625+
626+
// Arrange - render the FetchData component
627+
var cut = RenderComponent<FetchData>();
628+
629+
// Act - pass the test forecasts to the component via the mock services
630+
WaitForNextRender(() => mockForecastService.Task.SetResult(forecasts));
631+
632+
// Assert
633+
// Render an new instance of the ForecastDataTable, passing in the test data
634+
var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
635+
// Assert that the CUT has two changes, one removal of the loading message and one addition which matched the
636+
// rendered HTML from the expectedDataTable.
637+
cut.GetChangesSinceFirstRender().ShouldHaveChanges(
638+
diff => diff.ShouldBeRemoval("<p><em>Loading...</em></p>"),
639+
diff => diff.ShouldBeAddition(expectedDataTable)
640+
);
641+
}
642+
}
643+
```
644+
645+
- In `Test001` we use the `Services.AddService` method to register the dependency and the performs a regular "initial render" verification.
646+
647+
- `Test002` creates a new instance of the mock service and registers that with the the service provider. It then renders the CUT and uses `WaitForNextRender` to pass the test data to the mock services task, which then completes and the CUT gets the data.
648+
649+
- In the assert step we expect the CUT to use a `ForecastDataTable` to render the forecast data. Thus, to make our assertion more simple and stable to changes, we render an instance of the `ForecastDataTable` use that to verify that the expected addition after the CUT receives the forecast data is as it should be.

sample/tests/CodeOnlyTests/Components/FetchDataTest.cs renamed to sample/tests/CodeOnlyTests/Pages/FetchDataTest.cs

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -12,62 +12,45 @@ namespace Egil.RazorComponents.Testing.SampleApp.CodeOnlyTests
1212
{
1313
public class FetchDataTest : ComponentTestFixture
1414
{
15-
[Fact]
16-
public void InitialLoadingHtmlRendersCorrectly()
15+
[Fact(DisplayName = "Fetch data component renders expected initial markup")]
16+
public void Test001()
1717
{
18+
// Arrange - add the mock forecast service
1819
Services.AddService<IWeatherForecastService, MockForecastService>();
20+
21+
// Act - render the FetchData component
1922
var cut = RenderComponent<FetchData>();
20-
var initialExpectedHtml = @"<h1>Weather forecast</h1>
21-
<p>This component demonstrates fetching data from a service.</p>
22-
<p><em>Loading...</em></p>";
2323

24+
// Assert that it renders the initial loading message
25+
var initialExpectedHtml = @"<h1>Weather forecast</h1>
26+
<p>This component demonstrates fetching data from a service.</p>
27+
<p><em>Loading...</em></p>";
2428
cut.ShouldBe(initialExpectedHtml);
2529
}
2630

27-
[Fact]
28-
public void AfterDataLoadsItIsDisplayedInAForecastTable()
31+
[Fact(DisplayName = "After data loads it is displayed in a ForecastTable component")]
32+
public void Test002()
2933
{
30-
// setup mock
31-
var mockForecastService = new MockForecastService();
32-
Services.AddService<IWeatherForecastService>(mockForecastService);
33-
34-
// arrange
34+
// Setup the mock forecast service
3535
var forecasts = new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Testy", TemperatureC = 42 } };
36-
var cut = RenderComponent<FetchData>();
37-
var initialHtml = cut.GetMarkup();
38-
39-
// act
40-
WaitForNextRender(() => mockForecastService.Task.SetResult(forecasts));
41-
42-
// assert
43-
var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
44-
cut.CompareTo(initialHtml)
45-
.ShouldHaveChanges(
46-
diff => diff.ShouldBeRemoval("<p><em>Loading...</em></p>"),
47-
diff => diff.ShouldBeAddition(expectedDataTable)
48-
);
49-
}
50-
51-
[Fact]
52-
public void AfterDataLoadsItIsDisplayedInAForecastTableChangeTracking()
53-
{
54-
// setup mock
5536
var mockForecastService = new MockForecastService();
5637
Services.AddService<IWeatherForecastService>(mockForecastService);
5738

58-
// arrange
59-
var forecasts = new[] { new WeatherForecast { Date = DateTime.Now, Summary = "Testy", TemperatureC = 42 } };
39+
// Arrange - render the FetchData component
6040
var cut = RenderComponent<FetchData>();
61-
var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
6241

63-
// act
42+
// Act - pass the test forecasts to the component via the mock services
6443
WaitForNextRender(() => mockForecastService.Task.SetResult(forecasts));
6544

66-
// assert
45+
// Assert
46+
// Render an new instance of the ForecastDataTable, passing in the test data
47+
var expectedDataTable = RenderComponent<ForecastDataTable>((nameof(ForecastDataTable.Forecasts), forecasts));
48+
// Assert that the CUT has two changes, one removal of the loading message and one addition which matched the
49+
// rendered HTML from the expectedDataTable.
6750
cut.GetChangesSinceFirstRender().ShouldHaveChanges(
6851
diff => diff.ShouldBeRemoval("<p><em>Loading...</em></p>"),
6952
diff => diff.ShouldBeAddition(expectedDataTable)
7053
);
7154
}
7255
}
73-
}
56+
}

sample/tests/MockForecastService.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
namespace Egil.RazorComponents.Testing.SampleApp
66
{
7-
internal class MockForecastService : IWeatherForecastService
8-
{
9-
public TaskCompletionSource<WeatherForecast[]> Task { get; } = new TaskCompletionSource<WeatherForecast[]>();
7+
internal class MockForecastService : IWeatherForecastService
8+
{
9+
public TaskCompletionSource<WeatherForecast[]> Task { get; } = new TaskCompletionSource<WeatherForecast[]>();
1010

11-
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate) => Task.Task;
12-
}
11+
public Task<WeatherForecast[]> GetForecastAsync(DateTime startDate) => Task.Task;
12+
}
1313
}

0 commit comments

Comments
 (0)