Test des composants Blazor avec bUnit et JustMock
Dans ce guide sur les tests unitaires des composants Blazor, nous couvrirons tout, de la configuration de vos projets aux simples exemples de tests unitaires et à un scénario principal.
Blazor est un sujet brûlant de nos jours, et ce c'est pourquoi j'ai décidé d'expérimenter ce qui est possible à ce stade précoce en ce qui concerne les tests unitaires. À ma grande surprise, il existe déjà une bibliothèque nommée bUnit pour le rendu des composants testés. Et JustMock Lite avec JustMock étaient parfaitement capables de se moquer de tout ce que j'ai essayé.
Dans cet article, je vais vous montrer quelques exemples simples qui pourraient vous intéresser. Tout d'abord, je vais commencer par configurer vos projets, continuer avec des exemples de tests unitaires simples et terminer avec un scénario maître-détail.
Commençons par configurer vos projets. Comme pour toute autre technologie, vous aurez besoin d'au moins deux projets. Le premier est l'application Blazor et le second est destiné aux tests unitaires.
Création du projet d'application Blazor
La création de votre projet Blazor ne devrait pas poser de problème car il existe un modèle dans Visual Studio 2019.
Le projet nouvellement créé contient des pages par défaut et des classes de données que nous allons légèrement étendre et utiliser pour les exemples.
Création du projet de test unitaire
La configuration du projet de test unitaire n'est pas aussi simple que l'application Blazor. Suivez simplement les étapes ci-dessous et tout ira bien.
- Comme première étape, créez une bibliothèque de classes Razor ciblant .NET Core 3.1. La même version devrait également s'appliquer à l'application Blazor.
- Ajoutez le package NuGet Microsoft.NET.Test.Sdk au projet.
- Ajoutez bUnit en tant que NuGet package au projet. Veuillez noter que ce package est toujours en version bêta et que vous devrez cocher la case Inclure la version préliminaire. Lorsque vous essayez d'ajouter le package, vous remarquerez l'erreur suivante:
<
PropertyGroup
>
<
TargetFramework
> netcoreapp3.1 </
TargetFramework
>
</
PropertyGroup
> [[19659027] Et encore une fois, essayez d'ajouter le package bUnit NuGet.
- Ajoutez xUnit en tant que package NuGet au projet. J'utilise xUnit car j'ai vu qu'il était pris en charge par bUnit.
- Ajouter le package NuGet xunit.runner.visualstudio au projet. Nous avons besoin de ce package pour exécuter les tests unitaires dans Visual Studio.
- Ajoutez le Ju stMock Package NuGet (il s'agit de la version gratuite de JustMock - pour des raisons historiques, elle n'est pas renommée JustMock Lite).
- Ajoutez une référence à l'application Blazor Demo.
- Générez pour valider qu'il n'y a pas d'erreur.
OK, nous avons créé le projet de test et nous l'avons construit avec succès. Nous devons maintenant préparer la classe de test.
- Ajoutez une classe qui sera utilisée pour les tests unitaires
- Ajoutez des utilisations à Bunit, Xunit et Telerik.JutMock
- Rendez la classe publique
- Héritez le résumé ComponentTestFixture class
Nous sommes maintenant prêts à commencer à écrire notre premier test.
Le test unitaire le plus simple
Si vous lancez l'application Blazor Demo que nous avons ajoutée, vous remarquerez que dans la navigation de gauche, il y a trois liens. Accueil, compteur et récupération de données. Je vais tester la fonctionnalité dans la page Counter où cliquer sur un bouton augmentera le compteur.
Pour ce faire, je devrai d'abord rendre la page Counter. Trouvez où se trouve la valeur du compteur. Validez qu'il s'agit de zéro. Cliquez sur le bouton et validez que la valeur du compteur est passée à 1. Voici à quoi ressemblera ce test:
[Fact]
public
void
TestCounter ()
{
// Arrange
var cut = RenderComponent ();
cut.Find ([19659052] "p"
). MarkupMatches (
" Nombre actuel: 0
"
);
// Loi
var element = cut.Find (
"bouton" [19659017]);
element.Click ();
// Affirmer
cut.Find (
"p"
). MarkupMatches (
" Nombre actuel: 1
"
);
[19659004] }
Test d'un composant qui utilise des données
Dans l'application Blazor par défaut, il y a une page nommée FetchData qui utilise WeatherForecastService pour récupérer les données et les afficher. Je veux montrer à quel point il est utile d'utiliser bUnit pour obtenir certains composants et c'est pourquoi je déplacerai la partie avec la représentation des données de prévisions météorologiques dans un composant séparé. Voici à quoi ressemble le code:
@page
"/ fetchdata"
@
à l'aide de
BlazorDemoApp.Data
@
en utilisant
BlazorDemoApp.Components
@inject WeatherForecastService ForecastService
Prévisions météorologiques
Ce composant illustre la récupération de données à partir d'un service.
@
si
(prévisions ==
null
)
{
Chargement ...
}
sinon
{
<ForecastDataTable Forecasts =
"prévisions"
/>
}
@code {
privé
prévisions WeatherForecast [];
protégé
override
tâche asynchrone OnInitializedAsync ()
{
prévisions = attendre ForecastService .GetForecastAsync (DateTime.Now);
}
} [19659027] Et voici comment le ForecastDataTabl Le composant ressemble à:
<table
class
=
"forcast-data-table"
>
Date
Temp. (C)
Temp. (F)
Résumé
[19659117]
@
foreach
(var var
en
Prévisions)
{
[19659004]
@ prévision.Date.ToShortDateString ()
@fore cast.TempératureC
@ prévision.TempératureF
@ prevision.Summary
]
}
[1945902424]
@code {
privé [19659044] WeatherForecast [] _forecasts = Array.Empty
();
[Parameter]
public
WeatherForecast [] Prévisions
{
obtenez
=> _forecasts;
[19659174] set
=> _forecasts = value ?? Array.Vide
();
}
}
Maintenant, je peux commencer à écrire les tests. Pour cette page particulière, j'ai besoin d'écrire deux tests unitaires. Le premier doit tester que le texte de chargement est affiché lorsque la variable de prévisions est nulle. Et la seconde consiste à tester que les données s'affichent correctement lorsqu'il existe des données réelles.
Prévision dans un scénario nul
La première étape pour tester ce scénario consiste à enregistrer le WeatherForecastService.Services.AddSingleton
(); Après cela, je devrai rendre la page FetchData et valider que le texte de chargement est affiché. Voici à quoi ressemble le test unitaire:
[Fact]
public
void
TestFetchData_NullForecast ()
{
Services.AddSingleton
();
var cut = RenderComponent
();
]
// Affirmez qu'elle affiche le message de chargement initial
var initialExpectedHtml =
@ "
Prévisions météorologiques
Cette composante illustre l'extraction de données à partir d'un service.
Chargement en cours ...
";
cut.MarkupMatches (initialExpectedHtml);
}
Si vous l'exécutez testez maintenant, vous remarquerez qu'il échouera. La raison en est que le WeatherForecastService génère des valeurs aléatoires et la variable de prévisions dans le composant ne sera jamais nulle.
Ceci est un bon candidat pour se moquer. Si vous n'êtes pas familier avec le concept, en se moquant, pour tester la logique requise de manière isolée, les dépendances externes sont remplacées par des objets de remplacement étroitement contrôlés qui simulent le comportement des vrais. Pour notre scénario, la dépendance externe est l'appel à la méthode WeatherForecastService.GetForecastAsync.
Pour simuler cette méthode, je devrai utiliser un cadre de simulation comme Telerik JustMock . Il est capable de se moquer littéralement de tout - des API publiques aux API internes, privées ou statiques, même des membres de MsCorLib comme DateTime.Now et plus encore.
Il existe une version gratuite de JustMock nommée JustMock Lite . JustMock Lite a la limitation de pouvoir se moquer uniquement des méthodes virtuelles publiques et des interfaces publiques. Vous pouvez vérifier ce tableau de comparaison entre JustMock et JustMock Lite pour voir la différence.
Retour au code. Pour ce billet de blog, j'ai décidé d'utiliser JustMock Lite et à cause de cela, je devrai modifier le WeatherForecastService pour hériter d'une interface et travailler avec cette interface à la place. Voici à quoi cela ressemblera:
interface publique
IWeatherForecastService
{
[1945902828]
Tâche
GetForecastAsync (DateTime startDate);
}
Pour travailler avec cette interface au lieu de l'implémentation réelle, plusieurs modifications doivent être apportées fabriqué. Le WeatherForecastService devrait d'abord hériter de cette interface.
Ensuite, la page FetchData devrait l'utiliser. Voici le morceau de code qui doit être modifié:
@page
"/ fetchdata"
@
en utilisant
BlazorDemoApp.Data
@
en utilisant
BlazorDemoApp.Components
@inject IWeatherForecastService ForecastService
l'application BlazorDemo pour continuer à travailler, je dois modifier la méthode Startup.ConfigureServices et ajouter IWeatherForecastService en tant que service singleton avec l'implémentation WeatherForecastService. Comme ceci:
public
void
ConfigureServices (services IServiceCollection)
{
[1945902828]
services.AddRazorPages ();
services.AddServerSideBlazor ();
]
services.AddSingleton
();
}
Nous sommes maintenant prêts à créer la maquette.
Ce que je vais faire est de créer une maquette de type IWeatherForecastService et d'organiser la méthode GetForecastAsync pour tout argument DateTime pour renvoyer une valeur qui se traduira par une valeur nulle pour la variable de prévision. À la fin, l'instance simulée doit être enregistrée comme implémentation de notre interface. Voici à quoi ressemble le test:
[Fact]
public
void
TestFetchData_ForecastIsNull ()
{
// Arrange
var weatherForecastServiceMock = Mock.Create
();
Mock.Arrange (() => weatherForecastServiceMock.GetForecastAsync (Arg.IsAny
( )))
.Retours (
nouveau
TaskCompletionSource
(). Tâche);
Services.AddSingleton
(weatherForecastServiceMock);
// A ct
var cut = RenderComponent
();
// Affirme - qu'il affiche le message de chargement initial
var initialExpectedHtml =
@ "
Prévisions météorologiques
Ce composant illustre l'extraction de données à partir d'un service.
Chargement en cours ...
";
cut.MarkupMatches (initialExpectedHtml);
[19659004]}
La prévision a une valeur
Pour ce scénario, je vais créer une maquette du GetForecastAsync de manière similaire à ce que j'ai fait lors du test précédent, mais cette fois, la méthode reviendra une seule valeur prédéfinie. J'utiliserai cette valeur plus tard pour la validation.
Ensuite, j'enregistrerai IWeatherForecastService avec l'implémentation de la maquette créée. Après cela, je rendrai le composant FetchData. bUnit possède une API qui me permet de rechercher un composant imbriqué dans un autre composant. C'est ce que je vais faire car j'ai déjà extrait la représentation des données prévisionnelles dans un autre composant. À la fin, je comparerai le résultat réel avec la valeur attendue. Voici à quoi ressemblera ce test unitaire:
[Fact]
public
void
TestFetchData_PredefinedForecast ()
[19659004]{
// Arrange
var prévisions =
nouveau
[] {
nouveau
WeatherForecast {Date = DateTime.Now, Summary =
"Testy"
TemperatureC = 42}};
var weatherForecastServiceMock = Mock.Create
[19659004]();
Mock.Arrange (() => weatherForecastServiceMock.GetForecastAsync (Arg.IsAny
()))
.Returns (Task.FromResult
(prévisions));
Services.AddSingleton
(weatherForecastServiceMock);
// Act - rendre le composant FetchData
]
var cut = RenderComponent
();
var actualForcastDataTable = cut.FindComponent
(); // rechercher le composant
// Affirmer
var attenduDataTable = RenderComponent
((nom de (ForecastDataTable.Forecasts), prévisions));
actualForcastDataTable.MarkupMatches (attenduDataTable.Markup);
}
Scénario maître-détail
Le dernier scénario que je voulais montrer est comment tester un maître-détail. Pour gagner du temps dans le développement de ce boîtier, j'utiliserai le Teleik Blazor Grid car il a un support maître-détail intégré. Pour pouvoir exécuter le scénario, vous devez télécharger la version d'essai de Telerik Blazor si vous ne disposez pas déjà d'une licence. Pour plus d'informations, vous pouvez lire l'article de documentation Ce dont vous avez besoin pour utiliser les composants Telerik Blazor .
Création de la page
Dans un premier temps, je dois créer une nouvelle page. J'utiliserai un nom MasterDetail pour cela. Voici à quoi ressemble le code de cette page:
@page
"/ master-detail"
@
en utilisant
BlazorDemoApp.Data
@
en utilisant
BlazorDemoApp.Components
@inject IWeatherForecastService ForecastService
]
Prévisions météorologiques
Ce composant illustre la récupération de données d'un service.
@
if
(prévisions ==
null
)
[19659004]{
Chargement ...
]
}
sinon
{
<ForecastDataGrid Forecasts =
"prévisions"
/>
}
@code {
privé
WeatherForecast [] prévisions;
]
protected
override
async Task OnInitializedAsync ()
{
[19659004]
prévisions = attendre ForecastService.GetForecastAsync (DateTime.Now);
}
}
Comme vous pouvez le voir, c'est le même que le précédent, à la seule différence qu'il utilise le composant ForecastDataGrid. Et voici à quoi ressemble le code du composant ForecastDataGrid:
<
div
class
=
"prévision-avec-grille-telerik"
>
<
TelerikGrid
Classe
=
"grille prévisionnelle"
Données
=
"@ Prévisions" [19659378] Pageable
=
"true"
PageSize
=
"10"
Triable
=
"true"
Hauteur
= [[19659052] "500px"
Réorganisable
=
"true"
Redimensionnable
=
"true "
Groupable
=
" true "
FilterMode
=
" GridFilterMode.FilterMenu "
>
<
GridColumns
>
[19659004]
<
GridColumn
Champ
=
"@ (nameof (WeatherForecast.Date))" "
Titre
=
" Date "
Largeur
=
" 100px "
Groupable
=
" false "
/>
<
GridColumn
Champ
=
"@ (nameof (WeatherForecast.TempératureC))" "
Title
=
" Temp. (C) "
Largeur
=
" 80px "
/>
<
GridColumn [19659378] Champ
=
"@ (nameof (WeatherForecast.TemperatureF))"
Titre
=
"Temp. (F) "
Largeur
=
" 80px "
/>
<
GridColumn [19659378] Champ
=
"@ (nameof (WeatherForecast.Summary))"
Titre
=
"Summary"
Largeur
=
"120px" [19659044] />
</
GridColumns
>
<
DetailTemplate
>
@ {
WeatherForecast weatherForecast = contexte comme WeatherForecast;
<
WeatherForecastDetail
WeatherFor ecast
=
"@ weatherForecast"
> </
WeatherForecastDetail
>
} [1945902525] ]
</
DetailTemplate
>
[1965904949] </
TelerikGrid
>
</
div
>
@code {
privé WeatherForecast [] _forecasts = Array .Vide <
WeatherForecast
> ();
[Parameter]
prévision météorologique publique [] Prévisions
{
get => _forecasts;
[19459117] set => _forecasts = valeur ?? Array.Vide <
WeatherForecast
> ();
}
}
Et voici à quoi ressemble le code du composant WeatherForecastDetail:
<div
class
=
"weather-Forecast-detail"
> [
@
si
(WeatherForecast! =
null
)
{
<div
classe
= [19659052] "rangée my-4"
>
<div
classe
=
"col-sm -12 "
>
<h3
class
=
"h1"
>
@ WeatherForecast.Date.ToString (
"jj MMMM aaaa"
nouveau
CultureInfo (
"en-US"
))
]
<div
classe
=
"row my-4"
>
<div
class
=
"col-sm-2"
>
<span
class
=
"small d-block text-muted"
> Temperature
in
Celsius
@ WeatherForecast.TemperatureC °
<div
classe
=
"col-sm-2"
>
]
<span
class
=
"small d-block text-muted"
> Temperature
in
Fahrenheit
@WeatherForecast.TemperatureF °
<div
class
=
"col-sm-2"
>
<span
class
=
"small d-block text-muted"
>Summary
@WeatherForecast.Summary
}
else
{
<div
class
=
"alert alert-primary"
role=
"alert"
>
Please select a weather forecast to see its details.
}
@code {
[Parameter]
public
WeatherForecast WeatherForecast {
get
;
set
; }
}
If you paste this code and build it, you will notice an error stating that the Telerik Grid can’t be found. To solve this add the Telerik.Blazor and Telerik.Blazor.Components usings to the _Imports.razor file.
@
using
Telerik.Blazor
@
using
Telerik.Blazor.Components
As a next step I must add the TelerikRootComponent in the MainLayout page. This is required to allow the Telerik Blazor Grid to work with detached popups as it was explained in the documentation article I've provided above. Here is how the MainLayout should look:
@inherits LayoutComponentBase
<
TelerikRootComponent
>
<
div
class
=
"sidebar"
>
<
NavMenu
/>
</
div
>
<
div
class
=
"main"
>
<
div
class
=
"top-row px-4"
>
</
div
>
<
div
class
=
"content px-4"
>
@Body
</
div
>
</
div
>
</
TelerikRootComponent
>
I have created the master detail page, and the build is passing. Now I need to add the new page to the navigation. To do so, I need to modify the NavMenu page and add the following:
<
li
class
=
"nav-item px-3"
>
<
NavLink
class
=
"nav-link"
href
=
"master-detail"
>
<
span
class
=
"oi oi-list-rich"
aria-hidden
=
"true"
></
span
> Master Detail
</
NavLink
>
</
li
>
Another thing that needs to be done is the registration of the Telerik Blazor service in the Startup.ConfigureServices method. Here is how the new code will look like:
public
void
ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddSingleton();
services.AddTelerikBlazor();
}
And lastly I need to add the JavaScript and CSS that are used by the Telerik Blazor components in the _Host.cshtml file. Here are the lines that needs to be added:
<
link
rel
=
"stylesheet"
href
=
"_content/Telerik.UI.for.Blazor/css/kendo-theme-default/all.css"
/>
<
script
src
=
"_content/telerik.ui.for.blazor/js/telerik-blazor.js"
defer></
script
>
The result is that in my BlazorDemo application I have a master detail page. Here is an image of the final result:
var cut = RenderComponent
CascadingValue(rootComponentMock)
);
After the rendering is done I will have to find the plus sign and click on it. Here is how:
IElement plusSymbol = cut.Find(
"tr.k-master-row td[data-col-index="0"]"
);
plusSymbol.Click();
Next I will make a separate rendering of the WeatherForecastDetail component with the predefined values. This rendering will be used as the expected value in the comparison. Here is how:
var expectedForecastDetal =
RenderComponent((nameof(WeatherForecastDetail.WeatherForecast), forecasts[0]));
And lastly, I will have to find the actual detail component and compare it to the expected one. Here is how:
var actualForecastDetailElement = cut.FindComponent();
actualForecastDetailElement.MarkupMatches(expectedForecastDetail);
And here is what the whole test looks like:
[Fact]
public
void
TestMasterDetail_CorrectValues()
{
// Arrange
var forecasts =
new
[] {
new
WeatherForecast { Date = DateTime.Now, Summary =
"Testy"
TemperatureC = 42 } };
var weatherForecastServiceMock = Mock.Create();
Mock.Arrange(() => weatherForecastServiceMock.GetForecastAsync(Arg.IsAny()))
.Returns(Task.FromResult(forecasts));
Services.AddMockJsRuntime();
Services.AddSingleton(weatherForecastServiceMock);
Services.AddTelerikBlazor();
var rootComponentMock = Mock.Create();
var cut = RenderComponent(
[19659117]CascadingValue(rootComponentMock)
);
// Act
IElement plusSymbol = cut.Find(
"tr.k-master-row td[data-col-index="0"]"
);
plusSymbol.Click();
// Assert
var expectedForecastDetail = RenderComponent((nameof(WeatherForecastDetail.WeatherForecast), forecasts[0]));
var actualForecastDeta ilElement = cut.FindComponent();
// find the component
actualForecastDetailElement.MarkupMatches(expectedForecastDetail);
}
You can find the code used for the examples in this github repo.
If you are interested in the topic and you want to see markup tests or more complex scenarios, please comment in the section bellow.
Source link