NOTE: Apart from
(and even then it's questionable, I'm Scottish). These are machine translated in languages I don't read. If they're terrible please contact me.
You can see how this translation was done in this article.
Sunday, 25 August 2024
//Less than a minute
Dans ce post, je vais commencer à ajouter des tests unitaires pour ce site. Ce ne sera pas un tutoriel complet sur le test d'unité, mais plutôt une série de messages sur la façon dont j'ajoute le test d'unité à ce site. Dans ce post, je teste certains services en se moquant de DbContext; c'est pour éviter les shennanigans spécifiques à la DB.
Unit Testing est un moyen de tester séparément les composants individuels de votre code. Ceci est utile pour un certain nombre de raisons:
Il y a un certain nombre d'autres types de tests que vous pouvez faire. Voici quelques-uns :
Je vais utiliser xUnit pour mes tests. Ceci est utilisé par défaut dans les projets ASP.NET Core. Je vais aussi utiliser Moq pour me moquer du DbContext avec
En préparation, j'ai ajouté une Interface pour mon DbContext. C'est pour que je puisse me moquer du DbContext dans mes tests. Voici l'interface :
namespace Mostlylucid.EntityFramework;
public interface IMostlylucidDBContext
{
public DbSet<CommentEntity> Comments { get; set; }
public DbSet<BlogPostEntity> BlogPosts { get; set; }
public DbSet<CategoryEntity> Categories { get; set; }
public DbSet<LanguageEntity> Languages { get; set; }
public DatabaseFacade Database { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
C'est assez simple, juste exposer nos DBSets et la méthode SaveChangesAsync.
Annexe I Ne fais pas ça. utiliser un modèle de dépôt dans mon code. Cela s'explique par le fait que le noyau du cadre d'entités est déjà un modèle de dépôt. J'utilise une couche de service pour interagir avec le DbContext. C'est parce que je ne veux pas effacer le pouvoir de l'Entity Framework Core.
Nous ajoutons ensuite une nouvelle classe à notre Mostlylucid.Test
projet avec une méthode d'extension pour configurer notre requête:
public static class MockDbSetExtensions
{
public static Mock<DbSet<T>> CreateDbSetMock<T>(this IEnumerable<T> sourceList) where T : class
{
// Use the MockQueryable.Moq extension method to create the mock
return sourceList.AsQueryable().BuildMockDbSet();
}
// SetupDbSet remains the same, just uses the updated CreateDbSetMock
public static void SetupDbSet<T>(this Mock<IMostlylucidDBContext> mockContext, IEnumerable<T> entities,
Expression<Func<IMostlylucidDBContext, DbSet<T>>> dbSetProperty) where T : class
{
var dbSetMock = entities.CreateDbSetMock();
mockContext.Setup(dbSetProperty).Returns(dbSetMock.Object);
}
}
Vous verrez que c'est en utilisant le MockQueryable.Moq
méthode d'extension pour créer la maquette. Ce qui met alors en place nos objets IQueryable et IAsyncQueryable.
Un principe fondamental des tests unitaires est que chaque test doit être une 'unité' de travail et ne pas dépendre du résultat d'un autre test (c'est pourquoi nous nous moquons de notre DbContext).
Dans notre nouvelle BlogServiceFetchTests
classe nous avons mis en place notre contexte de test dans le constructeur:
public BlogServiceFetchTests()
{
// 1. Setup ServiceCollection for DI
var services = new ServiceCollection();
// 2. Create a mock of IMostlylucidDbContext
_dbContextMock = new Mock<IMostlylucidDBContext>();
// 3. Register the mock of IMostlylucidDbContext into the ServiceCollection
services.AddSingleton(_dbContextMock.Object);
// Optionally register other services
services.AddScoped<IBlogService, EFBlogService>(); // Example service that depends on IMostlylucidDbContext
services.AddLogging(configure => configure.AddConsole());
services.AddScoped<MarkdownRenderingService>();
// 4. Build the service provider
_serviceProvider = services.BuildServiceProvider();
}
J'ai commenté ça assez fortement pour que vous puissiez voir ce qui se passe. Nous mettons en place un ServiceCollection
qui est une collection de services que nous pouvons utiliser dans nos tests. Nous créons alors une maquette de notre IMostlylucidDBContext
et de l'enregistrer dans le ServiceCollection
C'est ce que j'ai dit. Nous enregistrons ensuite tous les autres services dont nous avons besoin pour nos tests. Enfin, nous construisons le ServiceProvider
que nous pouvons utiliser pour obtenir nos services.
J'ai commencé par ajouter une seule classe de test, la ci-dessus BlogServiceFetchTests
En cours. C'est un cours de test pour le Post obtenant des méthodes de mon EFBlogService
En cours.
Chaque test utilise un commun SetupBlogService
méthode pour obtenir une nouvelle population EFBlogService
objet. C'est pour que nous puissions tester le service isolément.
private IBlogService SetupBlogService(List<BlogPostEntity>? blogPosts = null)
{
blogPosts ??= BlogEntityExtensions.GetBlogPostEntities(5);
// Setup the DbSet for BlogPosts in the mock DbContext
_dbContextMock.SetupDbSet(blogPosts, x => x.BlogPosts);
// Resolve the IBlogService from the service provider
return _serviceProvider.GetRequiredService<IBlogService>();
}
C'est une classe d'extension simple qui nous donne un certain nombre de pupulés BlogPostEntity
objets. C'est pour que nous puissions tester notre service avec un certain nombre d'objets différents.
public static List<BlogPostEntity> GetBlogPostEntities(int count, string? langName = "")
{
var langs = LanguageExtensions.GetLanguageEntities();
if (!string.IsNullOrEmpty(langName)) langs = new List<LanguageEntity> { langs.First(x => x.Name == langName) };
var langCount = langs.Count;
var categories = CategoryEntityExtensions.GetCategoryEntities();
var entities = new List<BlogPostEntity>();
var enLang = langs.First();
var cat1 = categories.First();
// Add a root post to the list to test the category filter.
var rootPost = new BlogPostEntity
{
Id = 0,
Title = "Root Post",
Slug = "root-post",
HtmlContent = "<p>Html Content</p>",
PlainTextContent = "PlainTextContent",
Markdown = "# Markdown",
PublishedDate = DateTime.ParseExact("2025-01-01T07:01", "yyyy-MM-ddTHH:mm", CultureInfo.InvariantCulture),
UpdatedDate = DateTime.ParseExact("2025-01-01T07:01", "yyyy-MM-ddTHH:mm", CultureInfo.InvariantCulture),
LanguageEntity = enLang,
Categories = new List<CategoryEntity> { cat1 }
};
entities.Add(rootPost);
for (var i = 1; i < count; i++)
{
var langIndex = (i - 1) % langCount;
var language = langs[langIndex];
var postCategories = categories.Take(i - 1 % categories.Count).ToList();
var dayDate = (i + 1 % 30 + 1).ToString("00");
entities.Add(new BlogPostEntity
{
Id = i,
Title = $"Title {i}",
Slug = $"slug-{i}",
HtmlContent = $"<p>Html Content {i}</p>",
PlainTextContent = $"PlainTextContent {i}",
Markdown = $"# Markdown {i}",
PublishedDate = DateTime.ParseExact($"2025-01-{dayDate}T07:01", "yyyy-MM-ddTHH:mm",
CultureInfo.InvariantCulture),
UpdatedDate = DateTime.ParseExact($"2025-01-{dayDate}T07:01", "yyyy-MM-ddTHH:mm",
CultureInfo.InvariantCulture),
LanguageEntity = new LanguageEntity
{
Id = language.Id,
Name = language.Name
},
Categories = postCategories
});
}
return entities;
}
Vous pouvez voir que tout ce que cela fait est de retourner un nombre défini de billets de blog avec des langues et des catégories. Cependant, nous ajoutons toujours un objet 'root' qui nous permet de nous fier à un objet connu dans nos tests.
Chaque test est conçu pour tester un aspect des résultats des poteaux.
Par exemple dans les deux ci-dessous, nous testons simplement que nous pouvons obtenir tous les messages et que nous pouvons obtenir des messages par langue.
[Fact]
public async Task TestBlogService_GetBlogsByLanguage_ReturnsBlogs()
{
var blogService = SetupBlogService();
// Act
var result = await blogService.GetPostsForLanguage(language: "es");
// Assert
Assert.Single(result);
}
[Fact]
public async Task TestBlogService_GetAllBlogs_ReturnsBlogs()
{
var blogs = BlogEntityExtensions.GetBlogPostEntities(2);
var blogService = SetupBlogService(blogs);
// Act
var result = await blogService.GetAllPosts();
// Assert
Assert.Equal(2, result.Count());
}
Un concept important dans les tests unitaires est "test d'échec" où vous établissez que votre code échoue de la manière que vous attendez de lui.
Dans les tests ci-dessous, nous testons d'abord que notre code de téléappel fonctionne comme prévu. Nous testons ensuite que si nous demandons plus de pages que nous avons, nous obtenons un résultat vide (et non une erreur).
[Fact]
public async Task TestBlogServicePagination_GetBlogsByCategory_ReturnsBlogs()
{
var blogPosts = BlogEntityExtensions.GetBlogPostEntities(10, "en");
var blogService = SetupBlogService(blogPosts);
// Act
var result = await blogService.GetPagedPosts(2, 5);
// Assert
Assert.Equal(5, result.Posts.Count);
}
[Fact]
public async Task TestBlogServicePagination_GetBlogsByCategory_FailsBlogs()
{
var blogPosts = BlogEntityExtensions.GetBlogPostEntities(10, "en");
var blogService = SetupBlogService(blogPosts);
// Act
var result = await blogService.GetPagedPosts(10, 5);
// Assert
Assert.Empty(result.Posts);
}
C'est un début simple pour notre test d'unité. Dans le prochain post, nous ajouterons des tests pour plus de services et de paramètres. Nous examinerons également comment nous pouvons tester nos paramètres à l'aide de tests d'intégration.