有效地重构测试程序,可以让 TDD 或撰写测试程序的生产力提升数倍。
本文介绍当使用 specflow 在进行整合测试或验收测试时,在 feature 文件上透过 tag 的标示,即可在 scenario 开始之前,以及 feature 结束之后,清除相关 table 的测试数据,以确保自动测试可重复执行无误。
实际上常透过 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();
}
}
}
第一次测试结果,通过测试
测试第二次,测试失败,duplicate key
第二次测试会失败,如测试报告中所描述,因为 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
数据表中的数据清掉。
但目前作法会碰到两个问题:
- 放在
BeforeScenario
里面,不需要清理数据的 Scenario 会无谓的执行TRUNCATE
命令,耗费性能。 - 要清理不同的 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]
的数据。
同理,需要设定 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或许您会对下列培训课程感兴趣:
- 2019/7/27(六)~2019/7/28(日):演化式设计:测试驱动开发与持续重构 第六梯次(中国台北)
- 2019/8/16(五)~2019/8/18(日):【C#进阶设计-从重构学会高易用性与高弹性API设计】第二梯次(中国台北)
- 2019/9/21(六)~2019/9/22(日):Clean Coder:DI 与 AOP 进阶实战 第二梯次(中国台北)
- 2019/10/19(六):【针对遗留代码加入单元测试的艺术】第七梯次(中国台北)
- 2019/10/20(日):【极速开发】第八梯次(中国台北)
想收到第一手公开培训课程资讯,或想询问企业内训、顾问、教练、咨询服务的,请洽 Facebook 粉丝专页:91敏捷开发之路。 |
原文:大专栏 [Specflow] TRUNCATE Table Test Data by Tag