SpecFlow和复杂对象

本文关键字:对象 复杂 SpecFlow | 更新日期: 2023-09-27 17:52:53

我正在评估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将从"给定"步骤实例化,并在"何时"answers"然后"步骤中使用?

Thanks in advance

EDIT:只是一个镜头在脑海中:嵌套表支持吗?

SpecFlow和复杂对象

我想说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        |

对于步骤后面的代码,您可以使用类似这样的代码,其中会有更多的错误处理。

    [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());
                }
            }
        }
    }

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

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

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

尽管如此,您仍然需要一种在规范中表达分层结构的方法,即使用Gherkin。据我所知,这是不可能的,从这篇文章(在SpecFlow谷歌组)看来,它之前已经讨论过了。

基本上,您可以创建自己的格式并在您的步骤中解析它。我还没有遇到这个自己,但我想我会尝试一个表与空白值的下一级和解析,在步骤定义。这样的:

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场景中专门使用的"测试模型"。测试模型应该:

  • 关注商业术语
  • 允许您创建易于阅读的场景
  • 提供业务术语和复杂领域模型之间的解耦层

让我们以Blog为例。

SpecFlow场景:创建博客文章

考虑下面的场景,以便任何熟悉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   | <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               |

这是一个复杂关系的模型,其中一个博客有许多博客文章。

领域模型

这个Blog应用程序的域模型是这样的:

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
}

注意,我们的场景有一个值为"Working Draft"的"Status",但是BlogPostStatus enum有WorkingDraft。如何将这种"自然语言"状态转化为枚举?现在输入Test Model。

测试模型:BlogPostRow

BlogPostRow类意味着做几件事:

  1. 将SpecFlow表转换为对象
  2. 用给定值更新域模型
  3. 提供一个"复制构造函数",用一个已有的域模型实例的值作为一个BlogPostRow对象的种子,这样你就可以在SpecFlow
  4. 中比较这些对象
代码:

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,反之亦然。

(说明:我知道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 -某种类型的持久数据存储,其生存期是当前场景)

更大上下文中的模型

退一步说,术语"模型"已经变得更加复杂了,我们刚刚介绍了另一种类型的模型。让我们看看他们是如何一起玩的:
  • 域模型:一个类对业务需要的东西建模,通常存储在数据库中,并包含对业务规则建模的行为。
  • 视图模型:您的域模型
  • 的以演示为中心的版本
  • 数据传输对象:用于将数据从一个层或组件传输到另一个层或组件的数据包(通常用于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模型绑定器的命名约定模式。这里有一个例子:没有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库的引用。

using 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>());
    }

可以使用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 - In your feature call the step sending the model fullname/assembly and the table data

feature step

3 - In the Steps class you can convert the table in a list o of objects.

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