SpecFlow和复杂对象

我正在评估SpecFlow ,我有点卡住了。
我发现的所有样品基本上都是简单的物体。

我正在开发的项目严重依赖于复杂的对象。 一个接近的样本可能是这个对象:

public class MyObject { public int Id { get; set; } public DateTime StartDate { get; set; } public DateTime EndDate { get; set; } public IList Children { get; set; } } public class ChildObject { public int Id { get; set; } public string Name { get; set; } public int Length { get; set; } } 

有没有人知道如何编写我的function/场景,其中MyObject将从“Given”步骤实例化并用于“When”和“Then”步骤?

提前致谢

编辑:只是一个想法:支持嵌套表?

对于你所展示的例子,我会说你错了 。 这个例子看起来更适合用nunit写,可能还有一个对象母 。 使用specflow或类似工具编写的测试应该面向客户,并使用与客户用于描述该function相同的语言。

我会说Marcus在这里非常正确,但是我会编写我的场景,以便我可以在TechTalk.SpecFlow.Assist命名空间中使用一些扩展方法。 看到这里 。

 Given I have the following Children: | Id | Name | Length | | 1 | John | 26 | | 2 | Kate | 21 | Given I have the following MyObject: | Field | Value | | Id | 1 | | StartDate | 01/01/2011 | | EndDate | 01/01/2011 | | Children | 1,2 | 

对于步骤后面的代码,你可以使用这样的东西,它会有更多的error handling。

  [Given(@"I have the following Children:")] public void GivenIHaveTheFollowingChildren(Table table) { ScenarioContext.Current.Set(table.CreateSet()); } [Given(@"I have entered the following MyObject:")] public void GivenIHaveEnteredTheFollowingMyObject(Table table) { var obj = table.CreateInstance(); var children = ScenarioContext.Current.Get>(); obj.Children = new List(); foreach (var row in table.Rows) { if(row["Field"].Equals("Children")) { foreach (var childId in row["Value"].Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries)) { obj.Children.Add(children .Where(child => child.Id.Equals(Convert.ToInt32(childId))) .First()); } } } } 

希望这(或其中一些)对你有所帮助

我建议您尽量保持场景的清洁,重点关注项目中非技术人员的可读性。 然后,在步骤定义中处理如何构造复杂对象图。

有了这个说你还需要一种方法来表达你的规范中的层次结构,即与Gherkin。 据我所知,这是不可能的,并且从这篇文章 (在SpecFlow Google小组中)看来它之前已经讨论过了。

基本上你可以发明自己的格式并在你的步骤中解析。 我自己没有碰到这个,但我想我会尝试一个下一级空白值的表,并在步骤定义中解析它。 像这样:

 Given I have the following hierarchical structure: | MyObject.Id | StartDate | EndDate | ChildObject.Id | Name | Length | | 1 | 20010101 | 20010201 | | | | | | | | 1 | Me | 196 | | | | | 2 | You | 120 | 

我承认这不是超级漂亮,但它可以工作。

另一种方法是使用默认值并给出差异。 像这样:

 Given a standard My Object with the following children: | Id | Name | Length | | 1 | Me | 196 | | 2 | You | 120 | 

在步骤定义中,然后为MyObject添加“标准”值并填写子项列表。 如果你问我这个方法有点可读,但是你必须“知道”标准的MyObject是什么以及它是如何配置的。

基本上 – 小黄瓜不支持它。 但是您可以创建一个可以自己解析的格式。

希望这能回答你的问题……

当我的域对象模型开始变得复杂时,我更进一步,并创建我在SpecFlow场景中专门使用的“测试模型”。 测试模型应该:

  • 专注于商业术语
  • 允许您创建易于阅读的方案
  • 在业务术语和复杂的域模型之间提供一层解耦

我们以博客为例。

SpecFlow场景:创建博客post

请考虑以下方案编写,以便任何熟悉Blog工作方式的人都知道发生了什么:

 Scenario: Creating a Blog Post Given a Blog named "Testing with SpecFlow" exists When I create a post in the "Testing with SpecFlow" Blog with the following attributes: | Field | Value | | Title | Complex Models | | Body | 

This is not so hard.

| | Status | Working Draft | Then a post in the "Testing with SpecFlow" Blog should exist with the following attributes: | Field | Value | | Title | Complex Models | | Body |

This is not so hard.

| | Status | Working Draft |

这模拟了复杂的关系,其中博客有许多博客post。

领域模型

此博客应用程序的域模型将是:

 public class Blog { public string Name { get; set; } public string Description { get; set; } public IList Posts { get; private set; } public Blog() { Posts = new List(); } } public class BlogPost { public string Title { get; set; } public string Body { get; set; } public BlogPostStatus Status { get; set; } public DateTime? PublishDate { get; set; } public Blog Blog { get; private set; } public BlogPost(Blog blog) { Blog = blog; } } public enum BlogPostStatus { WorkingDraft = 0, Published = 1, Unpublished = 2, Deleted = 3 } 

请注意,我们的场景具有“状态”,其值为“工作草稿”,但BlogPostStatus枚举具有WorkingDraft 。 你如何将这种“自然语言”状态翻译成枚举? 现在进入测试模型。

测试模型:BlogPostRow

BlogPostRow类用于执行以下操作:

  1. 将SpecFlow表转换为对象
  2. 使用给定值更新您的域模型
  3. 提供“复制构造函数”,使用现有域模型实例中的值为BlogPostRow对象设定种子,以便在SpecFlow中比较这些对象

码:

 class BlogPostRow { public string Title { get; set; } public string Body { get; set; } public DateTime? PublishDate { get; set; } public string Status { get; set; } public BlogPostRow() { } public BlogPostRow(BlogPost post) { Title = post.Title; Body = post.Body; PublishDate = post.PublishDate; Status = GetStatusText(post.Status); } public BlogPost CreateInstance(string blogName, IDbContext ctx) { Blog blog = ctx.Blogs.Where(b => b.Name == blogName).Single(); BlogPost post = new BlogPost(blog) { Title = Title, Body = Body, PublishDate = PublishDate, Status = GetStatus(Status) }; blog.Posts.Add(post); return post; } private BlogPostStatus GetStatus(string statusText) { BlogPostStatus status; foreach (string name in Enum.GetNames(typeof(BlogPostStatus))) { string enumName = name.Replace(" ", string.Empty); if (Enum.TryParse(enumName, out status)) return status; } throw new ArgumentException("Unknown Blog Post Status Text: " + statusText); } private string GetStatusText(BlogPostStatus status) { switch (status) { case BlogPostStatus.WorkingDraft: return "Working Draft"; default: return status.ToString(); } } } 

它位于私有GetStatusGetStatusText ,其中人类可读的博客post状态值被转换为枚举,反之亦然。

(披露:我知道Enum不是最复杂的案例,但它是一个易于理解的案例)

最后一个难题是步骤定义。

在步骤定义中使用测试模型和域模型

步:

 Given a Blog named "Testing with SpecFlow" exists 

定义:

 [Given(@"a Blog named ""(.*)"" exists")] public void GivenABlogNamedExists(string blogName) { using (IDbContext ctx = new TestContext()) { Blog blog = new Blog() { Name = blogName }; ctx.Blogs.Add(blog); ctx.SaveChanges(); } } 

步:

 When I create a post in the "Testing with SpecFlow" Blog with the following attributes: | Field | Value | | Title | Complex Models | | Body | 

This is not so hard.

| | Status | Working Draft |

定义:

 [When(@"I create a post in the ""(.*)"" Blog with the following attributes:")] public void WhenICreateAPostInTheBlogWithTheFollowingAttributes(string blogName, Table table) { using (IDbContext ctx = new TestContext()) { BlogPostRow row = table.CreateInstance(); BlogPost post = row.CreateInstance(blogName, ctx); ctx.BlogPosts.Add(post); ctx.SaveChanges(); } } 

步:

 Then a post in the "Testing with SpecFlow" Blog should exist with the following attributes: | Field | Value | | Title | Complex Models | | Body | 

This is not so hard.

| | Status | Working Draft |

定义:

 [Then(@"a post in the ""(.*)"" Blog should exist with the following attributes:")] public void ThenAPostInTheBlogShouldExistWithTheFollowingAttributes(string blogName, Table table) { using (IDbContext ctx = new TestContext()) { Blog blog = ctx.Blogs.Where(b => b.Name == blogName).Single(); foreach (BlogPost post in blog.Posts) { BlogPostRow actual = new BlogPostRow(post); table.CompareToInstance(actual); } } } 

TestContext – 某种持久性数据存储,其生命周期是当前场景)

更大背景下的模型

退后一步,“模型”一词变得更加复杂,我们刚刚介绍了一种模型。 让我们看看他们如何一起玩:

  • 域模型:建模业务通常存储在数据库中的类,并包含对业务规则建模的行为。
  • 视图模型:您的域模型的以演示为中心的版本
  • 数据传输对象:用于将数据从一个层或组件传输到另一个层或组件的数据包(通常用于Web服务调用)
  • 测试模型:用于以对阅读行为测试的业务人员有意义的方式表示测试数据的对象。 在域模型和测试模型之间进行转换。

您几乎可以将测试模型视为SpecFlow测试的视图模型,“视图”是用Gherkin编写的场景。

我现在已经在几个组织工作,这些组织都遇到了你在这里描述的同一个问题。 这是促使我(尝试)开始写一本关于这个主题的书的事情之一。

http://specflowcookbook.com/chapters/linking-table-rows/

在这里,我建议使用一个约定,允许您使用specflow表头来指示链接项的来源,如何识别您想要的那些,然后使用行的内容来提供数据以“查找”外国表。

例如:

 Scenario: Letters to Santa appear in the emailers outbox Given the following "Children" exist | First Name | Last Name | Age | | Noah | Smith | 6 | | Oliver | Thompson | 3 | And the following "Gifts" exist | Child from Children | Type | Colour | | Last Name is Smith | Lego Set | | | Last Name is Thompson | Robot | Red | | Last Name is Thompson | Bike | Blue | 

希望这会有所帮助。

一个好主意是在StepArgumentTransformation方法中重用标准MVC Model Binder的命名约定模式。 这是一个例子: 没有mvc可以进行模型绑定吗?

以下是代码的一部分(只是主要想法,没有任何validation和您的附加要求):

function:

 Then model is valid: | Id | Children[0].Id | Children[0].Name | Children[0].Length | Children[1].Id | Children[1].Name | Children[1].Length | | 1 | 222 | Name0 | 5 | 223 | Name1 | 6 | 

步骤:

 [Then] public void Then_Model_Is_Valid(MyObject myObject) { // use your binded object here } [StepArgumentTransformation] public MyObject MyObjectTransform(Table table) { var modelState = new ModelStateDictionary(); var model = new MyObject(); var state = TryUpdateModel(model, table.Rows[0].ToDictionary(pair => pair.Key, pair => pair.Value), modelState); return model; } 

这个对我有用。

当然,您必须具有对System.Web.Mvc库的引用。

使用TechTalk.SpecFlow.Assist;

https://github.com/techtalk/SpecFlow/wiki/SpecFlow-Assist-Helpers

  [Given(@"resource is")] public void Given_Resource_Is(Table payload) { AddToScenarioContext("payload", payload.CreateInstance()); }