Comment écrire des tests unitaires pour les contrôleurs ASP.NET Core qui utilisent réellement mon contexte de firebase database?

Il semble y avoir peu d’informations sur la manière d’écrire de bons tests unitaires pour les actions réelles du contrôleur ASP.NET Core. Des conseils sur la façon de faire ce travail pour de vrai?

J’ai un système qui semble fonctionner plutôt bien en ce moment, alors j’ai pensé le partager et voir si cela n’aidait pas quelqu’un d’autre. La documentation d’Entity Framework contient un article très utile qui indique la voie à suivre. Mais voici comment je l’ai incorporé dans une application de travail réelle.

1. Créez une application Web ASP.NET Core dans votre solution.

Il existe des tonnes d’excellents articles pour vous aider à démarrer. La documentation pour la configuration de base et l’échafaudage est très utile. À cette fin, vous souhaiterez créer une application Web avec des comptes d’utilisateurs individuels afin que votre ApplicationDbContext soit configuré pour fonctionner automatiquement avec EntityFramework.

1a. Échafauder un contrôleur

Utilisez les informations incluses dans la documentation pour créer un contrôleur simple avec des actions CRUD de base.

2. Créez une bibliothèque de classes séparée pour vos tests unitaires.

Dans votre solution, créez une nouvelle bibliothèque .NET Core et référencez votre nouvelle application Web. Dans mon exemple, le modèle que j’utilise s’appelle Company et utilise le CompaniesController .

2a. Ajoutez les packages nécessaires à votre bibliothèque de tests

Pour ce projet, j’utilise xUnit comme testeur, Moq pour les objects moqueurs et FluentAssertions pour faire des assertions plus significatives. Ajoutez ces trois bibliothèques à votre projet en utilisant NuGet Package Manager et / ou la console. Vous devrez peut-être les rechercher avec la case à cocher Show Prerelease cochée.

Vous aurez également besoin de deux packages pour utiliser la nouvelle option de firebase database Sqlite-InMemory d’EntityFramework. C’est la sauce secrète. Voici une liste des noms de paquets sur NuGet:

  • Microsoft.Data.Sqlite
  • Microsoft.EntityFramework Core .InMemory [italique ajouté]
  • Microsoft.EntityFramework Core .Sqlite [italique ajouté]

3. Configurez votre équipement de test

Selon l’article que j’ai mentionné plus tôt , il existe un moyen simple et élégant de configurer Sqlite pour qu’il fonctionne comme une firebase database relationnelle en mémoire, à partir de laquelle vous pouvez exécuter vos tests.

Vous voudrez écrire vos méthodes de test unitaire afin que chaque méthode dispose d’une nouvelle copie propre de la firebase database. L’article ci-dessus vous montre comment procéder de manière ponctuelle. Voici comment j’ai configuré mon appareil pour qu’il soit aussi sec que possible.

3a. Actions du contrôleur synchrone

J’ai écrit la méthode suivante qui me permet d’écrire des tests en utilisant le modèle Arranger / Act / Assert, chaque étape jouant le rôle de paramètre dans mon test. Vous trouverez ci-dessous le code de la méthode et les propriétés de classe pertinentes dans le TestFixture auquel elle fait référence, ainsi qu’un exemple de ce à quoi cela ressemble d’appeler le code.

 public class TestFixture { public SqliteConnection ConnectionFactory() => new SqliteConnection("DataSource=:memory:"); public DbContextOptions DbOptionsFactory(SqliteConnection connection) => new DbContextOptionsBuilder() .UseSqlite(connection) .Options; public Company CompanyFactory() => new Company {Name = Guid.NewGuid().ToSsortingng()}; public void RunWithDatabase( Action arrange, Func act, Action assert) { var connection = ConnectionFactory(); connection.Open(); try { var options = DbOptionsFactory(connection); using (var context = new ApplicationDbContext(options)) { context.Database.EnsureCreated(); // Arrange arrange?.Invoke(context); } using (var context = new ApplicationDbContext(options)) { // Act (and pass result into assert) var result = act.Invoke(context); // Assert assert.Invoke(result); } } finally { connection.Close(); } } ... } 

Voici à quoi ça ressemble d’appeler le code pour tester la méthode Create sur CompaniesController (j’utilise des noms de parameters pour m’aider à garder mes expressions droites, mais vous n’en avez pas ssortingctement besoin):

  [Fact] public void Get_ReturnsAViewResult() { _fixture.RunWithDatabase( arrange: null, act: context => new CompaniesController(context, _logger).Create(), assert: result => result.Should().BeOfType() ); } 

Ma classe CompaniesController nécessite un enregistreur, que je maquille avec Moq et que je stocke en tant que variable dans TestFixture.

3b. Actions du contrôleur asynchrone

Bien entendu, bon nombre des actions ASP.NET Core intégrées sont asynchrones. Pour utiliser cette structure avec ceux-ci, j’ai écrit la méthode ci-dessous:

 public class TestFixture { ... public async Task RunWithDatabaseAsync( Func arrange, Func> act, Action assert) { var connection = ConnectionFactory(); await connection.OpenAsync(); try { var options = DbOptionsFactory(connection); using (var context = new ApplicationDbContext(options)) { await context.Database.EnsureCreatedAsync(); if (arrange != null) await arrange.Invoke(context); } using (var context = new ApplicationDbContext(options)) { var result = await act.Invoke(context); assert.Invoke(result); } } finally { connection.Close(); } } } 

C’est presque exactement la même chose, juste installer avec des méthodes asynchrones et des serveurs. Ci-dessous, un exemple d’appel de ces méthodes:

  [Fact] public async Task Post_WhenViewModelDoesNotMatchId_ReturnsNotFound() { await _fixture.RunWithDatabaseAsync( arrange: async context => { context.Company.Add(CompanyFactory()); await context.SaveChangesAsync(); }, act: async context => await new CompaniesController(context, _logger).Edit(1, CompanyFactory()), assert: result => result.Should().BeOfType() ); } 

3c Actions asynchrones avec des données

Bien sûr, vous devrez parfois passer des données entre les étapes de test. Voici une méthode que j’ai écrite et qui vous permet de le faire:

 public class TestFixture { ... public async Task RunWithDatabaseAsync( Func> arrange, Func> act, Action assert) { var connection = ConnectionFactory(); await connection.OpenAsync(); try { object data; var options = DbOptionsFactory(connection); using (var context = new ApplicationDbContext(options)) { await context.Database.EnsureCreatedAsync(); data = arrange != null ? await arrange?.Invoke(context) : null; } using (var context = new ApplicationDbContext(options)) { var result = await act.Invoke(context, data); assert.Invoke(result, data); } } finally { connection.Close(); } } } 

Et bien sûr, un exemple d’utilisation de ce code:

  [Fact] public async Task Post_WithInvalidModel_ReturnsModelErrors() { await _fixture.RunWithDatabaseAsync( arrange: async context => { var data = new { Key = "Name", Message = "Name cannot be null", Company = CompanyFactory() }; context.Company.Add(data.Company); await context.SaveChangesAsync(); return data; }, act: async (context, data) => { var ctrl = new CompaniesController(context, _logger); ctrl.ModelState.AddModelError(data.Key, data.Message); return await ctrl.Edit(1, data.Company); }, assert: (result, data) => result.As() .ViewData.ModelState.Keys.Should().Contain((ssortingng) data.Key) ); } 

Conclusion

J’espère vraiment que cela aidera quelqu’un à se mettre debout avec C # et les nouvelles fonctionnalités impressionnantes d’ASP.NET Core. Si vous avez des questions, des critiques ou des suggestions, faites-le moi savoir! Je suis toujours nouveau dans ce domaine aussi, alors tout retour d’information constructif est précieux pour moi!