65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2018年11月17日

CPOL

6分钟阅读

viewsIcon

11009

downloadIcon

93

TDD 和 BDD 结合示例进行解释。

引言

在本文中,我将尝试解释什么是 TDD 以及它在开发过程中如何提供帮助。有很多资源和书籍都在这样做,但我将尝试通过一个简单的实际示例来介绍。这更像是一个“哲学”概述,而不是你在书中能读到的严格定义。严格的 TDD 方法论支持者可能会觉得这个解释有点不完整(对此表示抱歉……),但我认为这足以开始学习和理解基础知识。我的主要目的不是写另一本关于 TDD 的书,而是用清晰简单的语言解释它是什么,以便初学者也能理解并接受它。

完整的源代码可在 GitHub 上找到.

什么是 TDD

直接从维基百科的定义开始

引用

测试驱动开发TDD)是一个软件开发过程,它依赖于一个非常短的开发周期的重复:需求被转化为非常具体的测试用例,然后软件得到改进以通过新测试,仅此而已。这与允许添加未经测试满足需求的软件的软件开发相反。

清楚了吗?TDD 的主要目的是创建一个策略,其中测试将驱动开发过程,从而使编码更高效、更具生产力,并减少回归。

先决条件是将大任务分解成小的步骤,并使用单元测试进行开发。这允许你处理一小段代码,使其工作,然后将许多工作部件集成在一起。

TDD 的好处

将 TDD 引入你的编码体验将是一个转折点。以下是其中最重要的好处的简短列表:

  1. 专注于真正重要的点:你将被要求分解问题,这将有助于将注意力集中在最重要的事情上。
  2. 处理更简单的任务:每次处理单一、更小的任务可以简化故障排除并加快开发速度。你不会陷入编写所有代码然后某处不起作用但你不知道原因的境地。
  3. 简化集成:当多个完成的工作功能合并在一起时,将所有内容整合在一起将是一项令人愉悦且容易的任务。如果发生回归,你将提前知道哪部分代码有问题。
  4. 免费的测试:一旦整个任务完成,就会剩下许多单元测试,它们可以用作集成/单元测试来验证代码并避免回归。

TDD 不是什么

TDD 是一个很好的方法论,但它不是

  • 替代测试(单元测试、验收测试、UI 测试)
  • 一天就能学会的东西
  • 为你编写代码的东西
  • 一个能驱赶代码中 bug 的圣人

TDD 生命周期

TDD 主要由三个步骤组成:

  1. 编写单元测试(红色)。
  2. 使其工作(绿色)。
  3. 重构。

在示例中,你可以编写单元测试,使用其中的代码来实现功能直到它工作,然后进行重构,将这部分代码放在需要的地方。

步骤 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 的映射

  1. 编写实体。
  2. 编写 Wiki 页面的 DTO。
  3. 编写将实体映射到 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 的转换

  1. 创建一个将 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 增强映射

  1. 修改映射,添加 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:设置数据库迁移

  1. 运行 Add-Migration 脚本。
  2. 创建一个在内存中工作的单元测试来测试它。
    [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

  1. 编写一个 CRUD 测试。
  2. 进行测试。
    [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:测试服务

  1. 创建一个具有业务逻辑的服务。
  2. 进行测试。
    [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。因此,不是编写所有代码然后进行测试,而是将问题分解成多个子活动,并逐个测试它们。

未使用。

  1. 准备表单,并对其进行测试。
  2. 准备模型,测试从表单提交的内容是否填充了后端模型。
  3. 集成服务来保存数据,进行测试。

视图

  1. 准备模型,传递给视图,进行测试。
  2. 集成模型与服务,以获取真实数据。进行测试。

列表

  1. 准备视图模型,将假数据传递给 UI,进行测试。
  2. 集成服务,进行测试。

结论

TDD 是一个由测试支持的开发过程驱动的方法论。这在很多方面都有助于编码,但要求所有团队成员都具备一些基础知识。一旦达到这个阶段,你将处理更简单的任务,并且有许多可重用的测试。这个过程将有助于避免回归并更快地实现目标,即使在开发过程中编写单元测试需要付出努力。此外,如果由于复杂性而难以测试你的应用程序,你可以通过执行 BDD 来保持相同的理念。

历史

  • 2018-11-17:第一个版本
© . All rights reserved.