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 deBlazorDemoApp.Data
@en utilisantBlazorDemoApp.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éoverridetâche asynchrone OnInitializedAsync ()
{
prévisions = attendre ForecastService .GetForecastAsync (DateTime.Now);
}
} [19659027] Et voici comment le ForecastDataTabl Le composant ressemble à:
<tableclass="forcast-data-table">
Date
Temp. (C)
Temp. (F)
Résumé
[19659117]
@foreach(var varenPré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]
publicWeatherForecast [] 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]
publicvoidTestFetchData_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 publiqueIWeatherForecastService
{[1945902828]
TâcheGetForecastAsync (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 utilisantBlazorDemoApp.Data
@en utilisantBlazorDemoApp.Components
@inject IWeatherForecastService ForecastServicel'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:
publicvoidConfigureServices (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]
publicvoidTestFetchData_ForecastIsNull ()
{
// Arrange
var weatherForecastServiceMock = Mock.Create();
Mock.Arrange (() => weatherForecastServiceMock.GetForecastAsync (Arg.IsAny( )))
.Retours (nouveauTaskCompletionSource(). 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]
publicvoidTestFetchData_PredefinedForecast ()[19659004]{
// Arrange
var prévisions =nouveau[] {nouveauWeatherForecast {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 utilisantBlazorDemoApp.Data
@en utilisantBlazorDemoApp.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;
]protectedoverrideasync 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:
<divclass="prévision-avec-grille-telerik">
<TelerikGridClasse="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]<GridColumnChamp="@ (nameof (WeatherForecast.Date))" "Titre=" Date "Largeur=" 100px "Groupable=" false "/>
<GridColumnChamp="@ (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;
<WeatherForecastDetailWeatherFor 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:
<divclass="weather-Forecast-detail"> [
@si(WeatherForecast! =null)
{
<divclasse= [19659052] "rangée my-4">
<divclasse="col-sm -12 ">
<h3class="h1">
@ WeatherForecast.Date.ToString ("jj MMMM aaaa"nouveauCultureInfo ("en-US"))
]
<divclasse="row my-4">
<divclass="col-sm-2">
<spanclass="small d-block text-muted"> TemperatureinCelsius
@ 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:
![Master Detail Page" title="Master Detail Page"/></p>
<h3>Writing the Unit Test for the Master Detail Scenario</h3>
<p>What I want to verify is that the master detail is working as expected. For that purpose I want to make the unit test render the page, click on the plus sign and verify that the detail component is rendered with the correct data.</p>
<p>To achieve this I will again create a mock of IWeatherForecastService and arrange it to return a predefined single value which will be used later for comparison. For this case a mock of JsRuntime should be registered as a service. bUnit provides such mock out of the box and can be used by simply adding Services.AddMockJsRuntime(); I must register the TeleirkBlazor service as well.</p>
<p>As the Telerik Grid has a requirement for the root element to be TelerikRootComponent, I will create a mock of TelerikRootComponent and render the grid as nested component. Here is how:[19659077]var rootComponentMock = Mock.Create<TelerikRootComponent>();</code></span></p>
<p><span style=](https://i0.wp.com/blog.arcoptimizer.com/wp-content/uploads/2020/05/1589228698_307_test-des-composants-blazor-avec-bunit-et-justmock.png?w=660&ssl=1)
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
