【问题标题】:SpecFlow and complex objectsSpecFlow 和复杂对象
【发布时间】:2011-08-12 22:28:44
【问题描述】:

我正在评估 SpecFlow,但我有点卡住了。
我找到的所有样本基本上都是简单的对象。

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

public class MyObject
{
    public int Id { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public IList<ChildObject> Children { get; set; }

}

public class ChildObject
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Length { get; set; }
}

有谁知道如何编写我的功能/场景,其中MyObject 将从“给定”步骤实例化并用于“何时”和“那么”步骤?

提前致谢

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

【问题讨论】:

    标签: c# nunit bdd specflow


    【解决方案1】:

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

    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        |
    

    对于这些步骤背后的代码,您可以使用类似这样的代码来处理更多错误。

        [Given(@"I have the following Children:")]
        public void GivenIHaveTheFollowingChildren(Table table)
        {
            ScenarioContext.Current.Set(table.CreateSet<ChildObject>());
        }
    
    
        [Given(@"I have entered the following MyObject:")]
        public void GivenIHaveEnteredTheFollowingMyObject(Table table)
        {
            var obj = table.CreateInstance<MyObject>();
            var children = ScenarioContext.Current.Get<IEnumerable<ChildObject>>();
            obj.Children = new List<ChildObject>();
    
            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());
                    }
                }
            }
        }
    

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

    【讨论】:

    • 谢谢,我正在编写一个 Web API,而我的客户是一台机器。一切都经过单元测试(使用母亲模式),但我需要某种方式来接受这些服务。很好的帮助!
    • 对于那些想知道从哪里获得扩展方法 CreateSet 的人,您需要从包管理器控制台执行 Install-Package SpecFlow.Assist.Dynamic。
    【解决方案2】:

    对于您展示的示例,我会说you're cuking it wrong。这个例子看起来更适合用 nunit 编写,并且可能使用 object mother。使用 specflow 或类似工具编写的测试应面向客户,并使用与您的客户用于描述功能相同的语言。

    【讨论】:

    • 我同意,规范的语言(因此,行为和设置)应该是用户和域的语言。这个级别太低了。
    • 我正在前往文章中描述的最坏情况。非常感谢。很好的文章,更好的答案:)
    • 我同意使用与您的客户描述功能相同的语言。但我仍然认为使用 table 支持嵌套对象很好且可读,正如这篇文章中提到的这个人 joebuschmann.com/specflow-nested-tables-a-bad-idea
    【解决方案3】:

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

    话虽如此,您仍然需要一种在规范中表达层次结构的方法,即使用 Gherkin。据我所知,这是不可能的,this post(在 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 以及它是如何配置的。

    基本上 - Gherkin 不支持它。但是你可以创建一个你可以自己解析的格式。

    希望这能回答您的问题...

    【讨论】:

    • 谢谢。我非常喜欢你的想法。说实话,我已经开始实施它,直到 Lazydev 阻止我:) 再次感谢。为想法 +1
    【解决方案4】:

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

    • 专注于业务术语
    • 让您创建易于阅读的场景
    • 在业务术语和复杂领域模型之间提供一层解耦

    我们以博客为例。

    SpecFlow 场景:创建博客文章

    考虑以下编写的场景,以便熟悉博客工作方式的任何人都知道发生了什么:

    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   | <p>This is not so hard.</p> |
            | Status | Working Draft               |
        Then a post in the "Testing with SpecFlow" Blog should exist with the following attributes:
            | Field  | Value                       |
            | Title  | Complex Models              |
            | Body   | <p>This is not so hard.</p> |
            | Status | Working Draft               |
    

    这模拟了一个复杂的关系,其中一个博客有许多博客帖子。

    领域模型

    这个博客应用程序的领域模型是这样的:

    public class Blog
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public IList<BlogPost> Posts { get; private set; }
    
        public Blog()
        {
            Posts = new List<BlogPost>();
        }
    }
    
    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 中,人类可读的博客文章状态值被转换为枚举,反之亦然。

    (披露:我知道 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   | <p>This is not so hard.</p> |
        | 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<BlogPostRow>();
            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   | <p>This is not so hard.</p> |
        | 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<BlogPostRow>(actual);
            }
        }
    }
    

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

    更大范围内的模型

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

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

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

    【讨论】:

      【解决方案5】:

      我现在曾在多个组织工作过,它们都遇到了您在此处描述的相同问题。这是促使我(尝试)开始写一本关于这个主题的书的原因之一。

      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   |
      

      希望这会有所帮助。

      【讨论】:

      • 链接已损坏。
      【解决方案6】:

      一个好主意是在 StepArgumentTransformation 方法中重新使用标准 MVC 模型绑定器的命名约定模式。这是一个例子:Is model binding possible without mvc?

      这是代码的一部分(只是主要思想,没有任何验证和您的额外要求):

      在功能方面:

      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 库的引用。

      【讨论】:

        【解决方案7】:

        使用 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<Part>());
            }
        

        【讨论】:

          【解决方案8】:

          您可以使用 Json 语法。

          1 - 创建表扩展

          
              public static class TableExtensions
              {
                  public static List <object> ToObjectByJson(this Table table, string modelFullName)
                  {
                      var type = Type.GetType(modelFullName);
                      var jsonSerializerSettings = new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() };
          
                      var listOfObjects = new List<object>();
                      foreach(var row in table.Rows)
                      {
                          var dynamicObject = new ExpandoObject();
          
                          foreach (var header in table.Header)
                          {
                              var val = row[header];
          
                              if (IsValidJson(val))
                              {
                                  dynamicObject.TryAdd(header, JsonConvert.DeserializeObject(val, jsonSerializerSettings));
                              }
                              else
                              {
                                  dynamicObject.TryAdd(header, val);
                              }
                          }
          
                          var json = JsonConvert.SerializeObject(dynamicObject, Formatting.Indented, jsonSerializerSettings);
                          listOfObjects.Add(JsonConvert.DeserializeObject(json, type, jsonSerializerSettings));
                      }
          
                      return listOfObjects;
                  }
          
                  private static bool IsValidJson(string strInput)
                  {
                      if (string.IsNullOrWhiteSpace(strInput)) { return false; }
                      strInput = strInput.Trim();
                      if ((strInput.StartsWith("{") && strInput.EndsWith("}")) || //For object
                          (strInput.StartsWith("[") && strInput.EndsWith("]"))) //For array
                      {
                          try
                          {
                              var obj = JToken.Parse(strInput);
                              return true;
                          }
                          catch (JsonReaderException jex)
                          {
                              //Exception in parsing json
                              Console.WriteLine(jex.Message);
                              return false;
                          }
                          catch (Exception ex) //some other exception
                          {
                              Console.WriteLine(ex.ToString());
                              return false;
                          }
                      }
                      else
                      {
                          return false;
                      }
                  }
          
              }
          

          2 - 在您的功能调用中,发送模型全名/程序集和表数据的步骤

          feature step

          3 - 在 Steps 类中,您可以将表格转换为对象列表。

          [Given(@"informei o seguinte argumento do tipo '(.*)':")]
                  public void EOsSeguintesValor(string modelType, Table table)
                  {
                      var objects = table.ToObjectsByJson(modelType);
                  }

          【讨论】:

            猜你喜欢
            • 2013-01-31
            • 2014-04-12
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多