21

我正在评估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从“给定”步骤实例化并在“何时”和“那么”步骤中使用?

提前致谢

编辑:请记住:是否支持嵌套表?

4

8 回答 8

31

我会说 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());
                }
            }
        }
    }

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

于 2011-04-26T21:46:44.483 回答
20

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

于 2011-04-26T18:38:25.103 回答
10

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

话虽如此,您仍然需要一种在规范中表达层次结构的方法,即使用 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 以及它是如何配置的。

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

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

于 2011-04-26T11:13:57.757 回答
6

当我的领域对象模型开始变得复杂时,我会更进一步,并创建我在 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();
        }
    }
}

它是私有GetStatus的,GetStatusText人类可读的博客文章状态值被转换为枚举,反之亦然。

(披露:我知道 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 编写的场景。

于 2015-09-28T18:04:07.370 回答
3

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

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   |

希望这会有所帮助。

于 2014-01-16T11:24:30.460 回答
1

一个好主意是在 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 库。

于 2015-07-27T14:26:47.670 回答
0

使用 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>());
    }
于 2017-05-19T21:56:30.557 回答
0

您可以使用 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 - 在您的功能调用中发送模型全名/程序集和表数据的步骤

特征步骤

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

[Given(@"informei o seguinte argumento do tipo '(.*)':")]
        public void EOsSeguintesValor(string modelType, Table table)
        {
            var objects = table.ToObjectsByJson(modelType);
        }
于 2020-12-26T16:34:11.990 回答