Skip to content

搞英语 → 看世界

翻译英文优质信息和名人推特

Menu
  • 首页
  • 作者列表
  • 独立博客
  • 专业媒体
  • 名人推特
  • 邮件列表
  • 关于本站
Menu

以正确方式编写单元测试的最佳实践(第 2 部分)

Posted on 2022-05-19

在上一篇文章以正确方式编写单元测试的最佳实践(第 1 部分)中,我们探讨了单元测试的一些最佳实践,然后编制了一个必备库列表,这些库可以极大地提高测试质量。但是,它没有涵盖一些常见的场景,比如测试 LINQ 和映射,所以我决定用另外一篇示例驱动的帖子来填补这个空白。

\ 准备好进一步提高您的单元测试技能了吗?让我们开始吧?

\ 本文中的示例广泛使用了上一篇文章中描述的出色工具,因此最好从第 1 部分开始,这样我们将要分析的代码更有意义。

测试 LINQ

事实上,所有 C# 开发人员都喜欢 LINQ,但我们也应该尊重它,并用测试覆盖查询。顺便说一句,这是 LINQ 相对于 SQL 的众多优势之一(您见过为 SQL 编写至少一个单元测试的真人吗?我也没有)。

\ 让我们看一个例子。

 public class UserRepository : IUserRepository { private readonly IDb _db; public UserRepository(IDb db) { _db = db; } public Task<User?> GetUser(int id, CancellationToken ct = default) { return _db.Users .Where(x => x.Id == id) .Where(x => !x.IsDeleted) .FirstOrDefaultAsync(ct); } // other methods }

在这个例子中,我们有一个典型的存储库,它有一个按 ID 返回用户的方法, _db.Users返回IQueryable<User> 。那么我们需要在这里测试什么?

\

  1. 如果用户没有被删除,我们希望确保此方法按 ID 返回用户。
  2. 如果具有给定 ID 的用户存在,则该方法返回null ,但被标记为已删除。
  3. 如果具有给定 ID 的用户不存在,则该方法返回null 。

\ 换句话说,所有的Where 、 OrderBy和其他方法调用都必须被测试覆盖。现在让我们编写并讨论第一个测试(?提醒:测试结构在上一篇文章中解释过):

 public class UserRepositoryTests { public class GetUser : UserRepositoryTestsBase { [Fact] public async Task Should_return_user_by_id_unless_deleted() { // arrange var expectedResult = F.Build<User>() .With(x => x.IsDeleted, false) .Create(); var allUsers = F.CreateMany<User>().ToList(); allUsers.Add(expectedResult); Db.Users.Returns(allUsers.Shuffle().AsQueryable()); // act var result = await Repository.GetUser(expectedResult.Id); // assert result.Should().Be(expectedResult); } [Fact] public async Task Should_return_null_when_user_is_deleted() { // see below } [Fact] public async Task Should_return_null_when_user_doesnt_exist() { // see below } } public abstract class UserRepositoryTestsBase { protected readonly Fixture F = new(); protected readonly UserRepository Repository; protected readonly IDb Db; protected UserRepositoryTestsBase() { Db = Substitute.For<IDb>(); Repository = new UserRepository(Db); } } }

首先,我们创建了一个满足要求的用户(未删除)并将其添加到一堆其他用户中(具有随机不同的 ID 和IsDeleted值)。然后我们模拟数据源以返回打乱后的数据集。请注意,我们打乱了用户列表以将expectedResult放置在随机位置。最后,我们调用了Repository.GetUser并验证了结果。

\ Shuffle()是一个小而有用的扩展方法:

\

 public static class EnumerableExtensions { private static readonly Random _randomizer = new(); public static T GetRandomElement<T>(this ICollection<T> collection) { return collection.ElementAt(_randomizer.Next(collection.Count)); } public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> objects) { return objects.OrderBy(_ => Guid.NewGuid()); } }

\ 第二个测试几乎与第一个相同。

\

 [Fact] public async Task Should_return_null_when_user_is_deleted() { // arrange var testUser = F.Build<User>() .With(x => x.IsDeleted, true) .Create(); var allUsers = F.CreateMany<User>().ToList(); allUsers.Add(testUser); Db.Users.Returns(allUsers.Shuffle().AsQueryable()); // act var result = await Repository.GetUser(testUser.Id); // assert result.Should().BeNull(); }

\ 这里我们将我们的用户标记为已删除,并检查结果是否为null 。

\ 对于最后一个测试,我们生成一个随机用户列表和一个不属于任何用户的唯一 ID:

\

 [Fact] public async Task Should_return_null_when_user_doesnt_exist() { // arrange var allUsers = F.CreateMany<User>().ToList(); var userId = F.CreateIntNotIn(allUsers.Select(x => x.Id).ToList()); Db.Users.Returns(allUsers.Shuffle().AsQueryable()); // act var result = await Repository.GetUser(userId); // assert result.Should().BeNull(); }

\ CreateIntNotIn()是另一个经常在测试中使用的有用方法:

\

 public static int CreateIntNotIn(this Fixture f, ICollection<int> except) { var maxValue = except.Count * 2; return Enumerable.Range(1, maxValue) .Except(except) .ToList() .GetRandomElement(); }

\ 让我们运行我们的测试:

\ LINQ 测试结果

✅ 看起来够绿了,让我们继续下一个例子。

测试映射 (AutoMapper)

我们首先需要对映射进行测试吗?

尽管许多开发人员声称这很无聊或浪费时间,但我相信映射的单元测试在开发过程中起着关键作用,原因如下:

  1. 很容易忽略数据类型中微小但重要的差异。例如,当 A 类的属性是DateTimeOffset类型,而 B 类的相应属性是DateTime类型时。默认映射不会崩溃,但会产生不正确的结果。

  2. 新的或删除的属性。使用映射测试,每当我们重构其中一个类时,就不可能忘记更改另一个类(因为编写良好的测试不会通过)。

  3. 错别字和不同的拼写。我们都是人类,通常不会注意到拼写错误,这反过来又会导致错误的映射结果。例子:

    \

 public class ErrorInfo { public string StackTrace { get; set; } public string SerializedException { get; set; } } public class ErrorOccurredEvent { public string StackTrace { get; set; } public string SerialisedException { get; set; } } public class ErrorMappings : Profile { public ErrorMappings() { CreateMap<ErrorInfo, ErrorOccurredEvent>(); } }

\ 在上面的代码中,很容易忽略不同拼写的问题,Rider / Resharper 也无济于事,因为 Seriali z ed 和 Seriali s ed 看起来都很好。在这种情况下,映射器将始终将目标属性设置为null ,这绝对是不可取的。

\ 我希望我设法说服了你并证明了单元测试对映射的价值,所以让我们继续下一个例子。我们将使用AutoMapper ,但从测试的角度来看,映射器的选择没有区别。例如,我们可以用Mapster替换 AutoMapper,它不会以任何方式影响我们的测试。此外,现有的测试将表明我们的映射重构是否成功,这是进行单元测试的要点之一?

\ 假设我们有这些实体:

 public class User { public int Id { get; init; } public string FirstName { get; set; } public string LastName { get; set; } public string Email { get; set; } public string Password { get; set; } public bool IsAdmin { get; set; } public bool IsDeleted { get; set; } } public class UserHttpResponse { public int Id { get; init; } public string Name { get; set; } public string Email { get; set; } public bool IsAdmin { get; set; } } public class BlogPost { public int Id { get; set; } public int UserId { get; set; } public DateTimeOffset CreatedAt { get; set; } public string Text { get; set; } } public class BlogPostDeletedEvent { public int Id { get; set; } public int UserId { get; set; } public DateTimeOffset CreatedAt { get; set; } public string Text { get; set; } } public class Comment { public int Id { get; set; } public int BlogId { get; set; } public int UserId { get; set; } public DateTimeOffset CreatedAt { get; set; } public string Text { get; set; } } public class CommentDeletedEvent { public int Id { get; set; } public int BlogId { get; set; } public int UserId { get; set; } public DateTimeOffset CreatedAt { get; set; } public string Text { get; set; } }

\ 和映射:

 public class MappingsSetup : Profile { public MappingsSetup() { CreateMap<User, UserHttpResponse>() .ForMember(x => x.Name, _ => _.MapFrom(x => $"{x.FirstName} {x.LastName}")); CreateMap<BlogPost, BlogPostDeletedEvent>(); CreateMap<Comment, CommentDeletedEvent>(); } }

\ 没什么特别花哨的: User >> UserHttpResponse的映射是稍微定制的,而另外两个是默认的“按原样映射”指令。让我们为我们的映射配置文件编写测试。

首先,这是可用于所有映射单元测试的基类:

\

 public abstract class MappingsTestsBase<T> where T : Profile, new() { protected readonly Fixture F; protected readonly IMapper M; public MappingsTestsBase() { F = new Fixture(); M = new MapperConfiguration(x => { x.AddProfile<T>(); }).CreateMapper(); } }

\ 以及我们对User >> UserHttpResponse映射的第一个测试:

\

 public class MappingsTests { public class User_TO_UserHttpResponse : MappingsTestsBase<MappingsSetup> { [Theory, AutoData] public void Should_map(User source) { // act var result = M.Map<UserHttpResponse>(source); // assert result.Name.Should().Be($"{source.FirstName} {source.LastName}"); result.Should().BeEquivalentTo(source, _ => _.Excluding(x => x.FirstName) .Excluding(x => x.LastName) .Excluding(x => x.Password) .Excluding(x => x.IsDeleted)); source.Should().BeEquivalentTo(result, _ => _.Excluding(x => x.Name)); } } }

\ 在这个测试中我们:

  1. 生成User类的随机实例。
  2. 将其映射到UserHttpResponse类型。
  3. 验证Name属性。
  4. 通过比较result ≡ source和source ≡ result来验证剩余的属性(为了不遗漏任何东西)。请注意,我们排除了任何类中不存在的每个属性,而不是使用ExcludingMissingMembers()排除具有拼写错误和不同拼写的属性(测试将无法检测到SerializedException与SerialisedException问题)。

具有相同属性的类(例如BlogPost >> BlogPostDeletedEvent )的默认映射测试可以用更通用和优雅的方式编写:

 public class SimpleMappings : MappingsTestsBase<MappingsSetup> { [Theory] [ClassData(typeof(MappingTestData))] public void Should_map(Type sourceType, Type destinationType) { // arrange var source = F.Create(sourceType, new SpecimenContext(F)); // act var result = M.Map(source, sourceType, destinationType); // assert result.Should().BeEquivalentTo(source); } private class MappingTestData : IEnumerable<object[]> { public IEnumerator<object[]> GetEnumerator() { return new List<object[]> { new object[] { typeof(BlogPost), typeof(BlogPostDeletedEvent) }, new object[] { typeof(Comment), typeof(CommentDeletedEvent) } } .GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } }

\ 您可能已经注意到可爱的[ClassData(typeof(MappingTestData))]属性。这是一种将MappingTestData类生成的测试数据与测试实现分开的干净方法。如您所见,为新的默认映射添加新测试只需要一行代码:

\

 return new List<object[]> { new object[] { typeof(BlogPost), typeof(BlogPostDeletedEvent) }, new object[] { typeof(Comment), typeof(CommentDeletedEvent) } } .GetEnumerator();

\ 很酷,不是吗?

最后的话

看起来你已经读到这里了?我希望它不会太无聊?

无论如何,今天我们已经处理了 LINQ 和映射的单元测试,结合上一篇文章以正确方式编写单元测试的最佳实践(第 1 部分)中描述的技术,提供了坚实的背景和对关键原则的理解用于编写干净、有意义且最重要的是有用的单元测试。

\ 干杯!

原文: https://hackernoon.com/best-practices-to-write-unit-tests-the-right-way-part-2?source=rss

本站文章系自动翻译,站长会周期检查,如果有不当内容,请点此留言,非常感谢。
  • Abhinav
  • Abigail Pain
  • Adam Fortuna
  • Alberto Gallego
  • Alex Wlchan
  • Answer.AI
  • Arne Bahlo
  • Ben Carlson
  • Ben Kuhn
  • Bert Hubert
  • Bits about Money
  • Brian Krebs
  • ByteByteGo
  • Chip Huyen
  • Chips and Cheese
  • Christopher Butler
  • Colin Percival
  • Cool Infographics
  • Dan Sinker
  • David Walsh
  • Dmitry Dolzhenko
  • Dustin Curtis
  • Elad Gil
  • Ellie Huxtable
  • Ethan Marcotte
  • Exponential View
  • FAIL Blog
  • Founder Weekly
  • Geoffrey Huntley
  • Geoffrey Litt
  • Greg Mankiw
  • Henrique Dias
  • Hypercritical
  • IEEE Spectrum
  • Investment Talk
  • Jaz
  • Jeff Geerling
  • Jonas Hietala
  • Josh Comeau
  • Lenny Rachitsky
  • Liz Danzico
  • Lou Plummer
  • Luke Wroblewski
  • Matt Baer
  • Matt Stoller
  • Matthias Endler
  • Mert Bulan
  • Mostly metrics
  • News Letter
  • NextDraft
  • Non_Interactive
  • Not Boring
  • One Useful Thing
  • Phil Eaton
  • Product Market Fit
  • Readwise
  • ReedyBear
  • Robert Heaton
  • Rohit Patel
  • Ruben Schade
  • Sage Economics
  • Sam Altman
  • Sam Rose
  • selfh.st
  • Shtetl-Optimized
  • Simon schreibt
  • Slashdot
  • Small Good Things
  • Taylor Troesh
  • Telegram Blog
  • The Macro Compass
  • The Pomp Letter
  • thesephist
  • Thinking Deep & Wide
  • Tim Kellogg
  • Understanding AI
  • 英文媒体
  • 英文推特
  • 英文独立博客
©2025 搞英语 → 看世界 | Design: Newspaperly WordPress Theme