[Specflow] TRUNCATE Table Test Data by Tag

有效地重构测试程序,可以让 TDD 或撰写测试程序的生产力提升数倍。

本文介绍当使用 specflow 在进行整合测试或验收测试时,在 feature 文件上透过 tag 的标示,即可在 scenario 开始之前,以及 feature 结束之后,清除相关 table 的测试数据,以确保自动测试可重复执行无误。

[Specflow] TRUNCATE Table Test Data by Tag


实际上常透过 specflow 搭配 Entity Framework 等 ORM 框架来进行数据面整合测试的初始化数据、清除数据与查询验证数据。范例如下所示:

Feature 文件内容

Feature: BookService


Scenario: Add the first book 
	Given a book for registering
	| ISBN          | Name  |
	| 9789869094481 | specification by example |	
	When Create
	Then Book table should exist a record
	| ISBN          | Name                     |
	| 9789869094481 | specification by example |	

Step Definitions 内容

    [Binding]
    public class BookServiceSteps
    {
        private BookService _bookService;

        [BeforeScenario()]
        public void BeforeScenario()
        {
            this._bookService = new BookService();
        }

        [Given(@"a book for registering")]
        public void GivenABookForRegistering(Table table)
        {
            var bookViewModel = table.CreateInstance();
            ScenarioContext.Current.Set(bookViewModel);
        }

        [When(@"Create")]
        public void WhenCreate()
        {
            var bookViewModel = ScenarioContext.Current.Get();
            this._bookService.Create(bookViewModel);
        }

        [Then(@"Book table should exist a record")]
        public void ThenBookTableShouldExistARecord(Table table)
        {
            using (var dbcontext = new NorthwindEntitiesForTest())
            {
                var book = dbcontext.Books.FirstOrDefault();
                Assert.IsNotNull(book);

                table.CompareToInstance(book);
            }
        }
    }

Production Code 内容

    internal class BookViewModel
    {
        public string ISBN { get; set; }
        public string Name { get; set; }
    }

    internal class BookService
    {
        public void Create(BookViewModel bookViewModel)
        {
            var book = new Book { ISBN = bookViewModel.ISBN, Name = bookViewModel.Name };

            //production and testing project shouldn't use the same connection string
            //it is just for sample code
            using (var dbcontext = new NorthwindEntitiesForTest())
            {
                dbcontext.Books.Add(book);
                dbcontext.SaveChanges();
            }
        }
    }

第一次测试结果,通过测试

[Specflow] TRUNCATE Table Test Data by Tag

测试第二次,测试失败,duplicate key

[Specflow] TRUNCATE Table Test Data by Tag

第二次测试会失败,如测试报告中所描述,因为 ISBN 是 PKey,不可重复。这是在整合测试很常会碰到的问题,因此我们需要在这类测试案例执行前后,清理 Book 表的数据。

使用 Entity Framework 清理测试数据

BeforeScenario 中,加入 Truncate 的命令,程序如下:

        [BeforeScenario()]
        public void BeforeScenario()
        {
            this._bookService = new BookService();
            using (var dbcontext = new NorthwindEntitiesForTest())
            {
                dbcontext.Database.ExecuteSqlCommand("TRUNCATE TABLE [Books]");

                dbcontext.SaveChangesAsync();
            }
        }

如此一来就能确保,每次测试案例执行前,都会将 Books 数据表中的数据清掉。

但目前作法会碰到两个问题:

  1. 放在 BeforeScenario 里面,不需要清理数据的 Scenario 会无谓的执行 TRUNCATE 命令,耗费性能
  2. 要清理不同的 table 中的测试数据时,能不能有弹性的重用这段程序。
针对这两个问题,我们可以透过使用 tag 标记来特定的 scenario 要清理哪些 table 来解决。

使用 Tag 来绑定要实现的逻辑

首先先把 CleanTable() 从 step definitions 中提炼出来,放到一个共用的 Hooks.cs 中。

[Binding]
public sealed class Hooks
{
    [BeforeScenario()]
    public void CleanTable()
    {
        using (var dbcontext = new NorthwindEntitiesForTest())
        {
            dbcontext.Database.ExecuteSqlCommand("TRUNCATE TABLE [Books]");

            dbcontext.SaveChangesAsync();
        }
    }
}

原本的 step definitions 就不需要再写任何清 table 的命令。

        [BeforeScenario()]
        public void BeforeScenario()
        {
            this._bookService = new BookService();            
        }

接下来,希望透过 tag 标记与 tag 名称来决定,这个 scenario 该先清除什么表的测试数据,把 Hooks 的 CleanTable() 加工一下。

    [BeforeScenario()]
    public void CleanTable()
    {
        var tags = ScenarioContext.Current.ScenarioInfo.Tags
            .Where(x => x.StartsWith("Clean"))
            .Select(x => x.Replace("Clean", ""));

        if (!tags.Any())
        {
            return;
        }

        using (var dbcontext = new NorthwindEntitiesForTest())
        {
            foreach (var tag in tags)
            {
                dbcontext.Database.ExecuteSqlCommand($"TRUNCATE TABLE [{tag}]");
            }

            dbcontext.SaveChangesAsync();
        }
    }
使用 ScenarioContext.Current.ScenarioInfo.Tags 取得所有 tag 后,筛选出 Clean 开头的 tag 并 parse table 名称,执行 truncate 命令。

接下来只需要在 scenario 上标记 tag,例如 @CleanBooks 就可以在 scenario 执行前清掉 table [Books] 的数据。

[Specflow] TRUNCATE Table Test Data by Tag

同理,需要设定 web UI testing browser 时,也可以用同样的方式处理,例如:

        [BeforeFeature()]
        [Scope(Tag = "web")]
        public static void SetBrowser()
        {
            SeleniumWebDriver.Bootstrap(
               SeleniumWebDriver.Browser.Chrome
           );
        }

只要有标记 @web 就会设定 browser 为 Chrome,当然,也可以透过多个 tag 来设定不同的 browser 种类。

结论

就像 ASP.NET MVC 的 ActionFilter 一样,透过 tag 做 Hook 除了让 feature 上有哪些背景作业要处理看起来可以一目了然,也可以让共用的程序独立维护、重复使用。

常见的应用,还有某些功能需要先登入后才能使用,除了在 feature 上用 Background 的方式标记外,也可以透过 tag 来处理。

开发过程内容,请参考 github repository: CleanTableByTag

或许您会对下列培训课程感兴趣:

  1. 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
  2. 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
  3. 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
  4. 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
  5. 2019/10/20(日):【极速开发】第八梯次(中国台北)
[Specflow] TRUNCATE Table Test Data by Tag

想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。

原文:大专栏  [Specflow] TRUNCATE Table Test Data by Tag


上一篇:SQLserver使用truncate语句报错: 找不到对象 "",因为它不存在或者您没有所需的权限。


下一篇:【Mysql】 Mysql 统计数据库大小及信息