Fermer

mai 11, 2020

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.

  1. 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.
  2. Ajoutez le package NuGet Microsoft.NET.Test.Sdk au projet.
  3. 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:

 Erreur d'installation de bUnit NuGet "title =" Erreur d'installation de bUnit NuGet "/> </p>
<p> En effet, le projet de bibliothèque de classes Razor cible la norme .NET. Modifiez-le. pour cibler l'application .NET core 3.1. Le résultat dans le fichier de projet doit être le suivant: </p>
<pre><code class=

< PropertyGroup >

< TargetFramework > netcoreapp3.1 </ TargetFramework >

</ PropertyGroup > [[19659027] Et encore une fois, essayez d'ajouter le package bUnit NuGet.

  1. Ajoutez xUnit en tant que package NuGet au projet. J'utilise xUnit car j'ai vu qu'il était pris en charge par bUnit.
  2. Ajouter le package NuGet xunit.runner.visualstudio au projet. Nous avons besoin de ce package pour exécuter les tests unitaires dans Visual Studio.
  3. 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).
  4. Ajoutez une référence à l'application Blazor Demo.
  5. 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.

  1. Ajoutez une classe qui sera utilisée pour les tests unitaires
  2. Ajoutez des utilisations à Bunit, Xunit et Telerik.JutMock
  3. Rendez la classe publique
  4. 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:

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=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