使用 ASP.NET 构建 Wiki 来解释 TDD





5.00/5 (4投票s)
TDD 和 BDD 结合示例进行解释。
引言
在本文中,我将尝试解释什么是 TDD 以及它在开发过程中如何提供帮助。有很多资源和书籍都在这样做,但我将尝试通过一个简单的实际示例来介绍。这更像是一个“哲学”概述,而不是你在书中能读到的严格定义。严格的 TDD 方法论支持者可能会觉得这个解释有点不完整(对此表示抱歉……),但我认为这足以开始学习和理解基础知识。我的主要目的不是写另一本关于 TDD 的书,而是用清晰简单的语言解释它是什么,以便初学者也能理解并接受它。
什么是 TDD
直接从维基百科的定义开始
引用测试驱动开发(TDD)是一个软件开发过程,它依赖于一个非常短的开发周期的重复:需求被转化为非常具体的测试用例,然后软件得到改进以通过新测试,仅此而已。这与允许添加未经测试满足需求的软件的软件开发相反。
清楚了吗?TDD 的主要目的是创建一个策略,其中测试将驱动开发过程,从而使编码更高效、更具生产力,并减少回归。
先决条件是将大任务分解成小的步骤,并使用单元测试进行开发。这允许你处理一小段代码,使其工作,然后将许多工作部件集成在一起。
TDD 的好处
将 TDD 引入你的编码体验将是一个转折点。以下是其中最重要的好处的简短列表:
- 专注于真正重要的点:你将被要求分解问题,这将有助于将注意力集中在最重要的事情上。
- 处理更简单的任务:每次处理单一、更小的任务可以简化故障排除并加快开发速度。你不会陷入编写所有代码然后某处不起作用但你不知道原因的境地。
- 简化集成:当多个完成的工作功能合并在一起时,将所有内容整合在一起将是一项令人愉悦且容易的任务。如果发生回归,你将提前知道哪部分代码有问题。
- 免费的测试:一旦整个任务完成,就会剩下许多单元测试,它们可以用作集成/单元测试来验证代码并避免回归。
TDD 不是什么
TDD 是一个很好的方法论,但它不是
- 替代测试(单元测试、验收测试、UI 测试)
- 一天就能学会的东西
- 为你编写代码的东西
- 一个能驱赶代码中 bug 的圣人
TDD 生命周期
TDD 主要由三个步骤组成:
- 编写单元测试(红色)。
- 使其工作(绿色)。
- 重构。
在示例中,你可以编写单元测试,使用其中的代码来实现功能直到它工作,然后进行重构,将这部分代码放在需要的地方。
步骤 1、2:让测试工作
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=StripHTML(test);
Assert.Equal(expected,result);
}
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
步骤 3:重构
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=HtmlHelper.StripHTML(test);
Assert.Equal(expected,result);
}
}
//somewhere else
public static class HtmlHelper
{
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
限制
在许多情况下,编写覆盖真实代码用例的单元测试是很困难的。对于完全逻辑性的过程很容易,但当我们涉及到数据库或 UI 时,编写的努力会增加,在许多情况下,可能会超过收益。有一些最佳实践和框架可以帮助解决这个问题,但总的来说,并非应用程序的所有部分都可以轻松地使用纯单元测试进行测试。
什么是 BDD?
BDD 是 TDD 的一种增强,它考虑了单元测试有限制的情况。这种扩展以开发人员为单元测试,遵循 BDD 的理念。你仍然可以将复杂任务分解成更小的任务,通过用户行为进行测试,并获得与纯后端任务使用 TDD 相同的优势。
TDD 的先决条件
在团队中工作时,所有队友都必须了解并接受这种方法论,并具备所涉及的所有技术知识。
首先,你的代码必须由强大的单元测试系统支持
- .NET, .NET Core:内置于 Visual Studio 或 xunit(后者是我个人偏好的选择)
- Java:junit 工作得非常好,我不需要寻找其他解决方案
- PHP:PHP unit 在所有情况下都对我有用
然后,重要且强制性的:拥有一个允许在测试期间模拟或重建正确行为的架构。我指的是一个可以在测试期间在内存中或在本地数据库中工作的 ORM,同时也使用服务或存储库模式。使用 DI 框架(内置的 .NET Core,autofac 或其他任何东西……)也有帮助。
最后但同样重要的是:一个良好的构建过程,集成到持续集成流程中,以及正确的配置来定义在集成期间哪些单元测试有意义,以及哪些仅在本地运行。
示例
让我们尝试在实际示例中实践我们学到的关于 TDD 的知识。我想用这种方法论创建一个 Wiki。我的意思是创建一个简单的 Wiki,用户可以登录、编写 markdown 页面并发布。
首先,我将把“长”任务分解成更小的连续活动。每个子部分将使用一个小单元测试进行开发。我将专注于 Wiki 页面的 CRUD。
步骤 1:实体到 DTO 的映射
- 编写实体。
- 编写 Wiki 页面的 DTO。
- 编写将实体映射到 DTO 的代码。
// Database entity
public class WikiPageEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
public string Body { get; set; }
public string Title { get; set; }
}
// DTO model in BLL
namespace WikiCore.Lib.DTO
{
public class WikiPageDTO
{
public string Title { get; set; }
public string BodyMarkDown { get; set; }
public string BodyHtml { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
}
}
// From unit test, code omitted for brevity
public void EntityToDTO()
{
WikiPageEntity source = new WikiPageEntity()
{
Title = "title",
Slug = "titleslug",
Version =1
};
var result = Mapper.Map<wikipagedto>(source);
Assert.Equal("title", result.Title);
Assert.Equal(1, result.Version);
}
// From Mapping configuration, code omitted for brevity
public MappingProfile()
{
CreateMap<wikipageentity, wikipagedto="">().ReverseMap();
}
步骤 2:Markdown 到 HTML 的转换
- 创建一个将
markdown
转换为 HTML 的方法。//Before refactoring public class MarkdownTest { [Fact] public void ConvertMarkDown() { var options = new MarkdownOptions { AutoHyperlink = true, AutoNewLines = true, LinkEmails = true, QuoteSingleLine = true, StrictBoldItalic = true }; Markdown mark = new Markdown(options); var testo = mark.Transform("#testo"); Assert.Equal("<h1>testo</h1>", testo); } // after refactoring ( method moved to helper [Fact] public void ConvertMarkDownHelper() { Assert.Equal("<h1>testo</h1>", MarkdownHelper.ConvertToHtml("#testo")); } // From markdown helper public static class MarkdownHelper { static MarkdownOptions options; static Markdown converter; static MarkdownHelper() { options = new MarkdownOptions { AutoHyperlink = true, AutoNewLines = true, LinkEmails = true, QuoteSingleLine = true, StrictBoldItalic = true }; converter = new Markdown(options); } public static string ConvertToHtml(string input) { Markdown mark = new Markdown(options); return mark.Transform(input); } }
步骤 3:通过 Markdown 增强映射
- 修改映射,添加 HTML 字段计算。
// mapped profile changed public class MappingProfile : Profile { public MappingProfile() { SlugHelper helper = new SlugHelper(); CreateMap<wikipageentity, wikipagedto="">() .ForMember(dest => dest.BodyMarkDown, (expr) => expr.MapFrom<string>(x => x.Body)) .ForMember(dest => dest.BodyHtml, (expr) => expr.MapFrom<string>(x => MarkdownHelper.ConvertToHtml(x.Body))) .ReverseMap(); CreateMap<wikipagebo,wikipageentity>() .ForMember(dest => dest.Body, (expr) => expr.MapFrom<string>(x => x.BodyMarkDown)) .ForMember(dest => dest.Slug, (expr) => expr.MapFrom<string>(x => helper.GenerateSlug(x.Title))); } } // From unit test, code omitted for brevity public void EntityToDTO() { WikiPageEntity source = new WikiPageEntity() { Body = "# prova h1", Title = "title", Slug = "titleslug", Version =1 }; var result = Mapper.Map<wikipagedto>(source); Assert.Equal("title", result.Title); Assert.Equal(1, result.Version); Assert.Equal("<h1>prova h1</h1>", result.BodyHtml); }
步骤 4:设置数据库迁移
- 运行
Add-Migration
脚本。 - 创建一个在内存中工作的单元测试来测试它。
[Fact] public void MigrateInMemory() { var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>(); optionsBuilder.UseInMemoryDatabase(); using (var db = new DatabaseContext(optionsBuilder.Options)) { db.Database.Migrate(); } // No error assert migration was OK }
步骤 5:实体 CRUD
- 编写一个 CRUD 测试。
- 进行测试。
[Fact] public void CrudInMemory() { var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>(); optionsBuilder.UseInMemoryDatabase(); using (var db = new DatabaseContext(optionsBuilder.Options)) { db.Database.Migrate(); db.WikiPages.Add(new Lib.DAL.Model.WikiPageEntity() { Title = "title", Body = "#h1", Slug = "slug" }); db.SaveChanges(); var count=db.WikiPages.Where(x => x.Slug == "slug").Count(); Assert.Equal(1, count); // update, delete steps omitted for brevity } }
步骤 6:测试服务
- 创建一个具有业务逻辑的服务。
- 进行测试。
[Fact] public void TestSave() { var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>(); optionsBuilder.UseInMemoryDatabase(); using (var db = new DatabaseContext(optionsBuilder.Options)) { db.Database.Migrate(); db.SaveChanges(); //this recreate same behaviour of asp.net MVC usage DatabaseWikiPageService service = new DatabaseWikiPageService(db, Mapper.Instance); service.Save(new Lib.BLL.BO.WikiPageBO() { BodyMarkDown="#h1", Title="prova prova" }); var item = service.GetPage("prova-prova"); Assert.NotNull(item); } }
步骤 7:继续 UI
一旦使用单元测试测试 UI 变得复杂,我就切换到 BDD,并采取多个步骤来完成 UI。因此,不是编写所有代码然后进行测试,而是将问题分解成多个子活动,并逐个测试它们。
未使用。
- 准备表单,并对其进行测试。
- 准备模型,测试从表单提交的内容是否填充了后端模型。
- 集成服务来保存数据,进行测试。
视图
- 准备模型,传递给视图,进行测试。
- 集成模型与服务,以获取真实数据。进行测试。
列表
- 准备视图模型,将假数据传递给 UI,进行测试。
- 集成服务,进行测试。
结论
TDD 是一个由测试支持的开发过程驱动的方法论。这在很多方面都有助于编码,但要求所有团队成员都具备一些基础知识。一旦达到这个阶段,你将处理更简单的任务,并且有许多可重用的测试。这个过程将有助于避免回归并更快地实现目标,即使在开发过程中编写单元测试需要付出努力。此外,如果由于复杂性而难以测试你的应用程序,你可以通过执行 BDD 来保持相同的理念。
历史
- 2018-11-17:第一个版本