SpecFlow and complex objects

前端 未结 7 798
旧巷少年郎
旧巷少年郎 2020-12-23 17:57

I\'m evaluating SpecFlow and I\'m a bit stuck.
All samples I have found are basically with simple objects.

Project I\'m working on heavily relies on a complex ob

相关标签:
7条回答
  • 2020-12-23 18:05

    I have worked at several organisations now that have all ran into the same issue you describe here. This is one of the things that prompted me to (attempt) to start writing a book on the subject.

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

    Here I suggest using a convention which allows you to use the specflow table headers to indicate where the linked items come from, how to identify which ones you want, and then use the content of the rows to provide the data to "lookup" in the foreign tables.

    For instance:

    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   |
    

    Hopefully this will be of some assistance.

    0 讨论(0)
  • 2020-12-23 18:09

    A good idea is to re-use the standard MVC Model Binder's naming convention pattern in a StepArgumentTransformation method. Here is an example: Is model binding possible without mvc?

    Here is part of the code (just the main idea, without any validations and your additional requirements):

    In features:

    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                  |
    

    In steps:

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

    It works for me.

    Of course you must have a reference to the System.Web.Mvc library.

    0 讨论(0)
  • 2020-12-23 18:14

    I would say that Marcus is pretty much correct here, however I would write my scenario so that I could use some of the extensions methods for in the TechTalk.SpecFlow.Assist namespace. See 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        |
    

    For the code behind the steps you could use something like this will a bit more error handling in it.

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

    Hope this (or some of this) help to you

    0 讨论(0)
  • 2020-12-23 18:17

    For the example you have shown I would say you're cuking it wrong. This example looks more suitable to write with nunit and probably using an object mother. Tests written with specflow or similar tool should be customer facing and use the same language as your customer would use to describe the feature.

    0 讨论(0)
  • 2020-12-23 18:18

    I go one step further when my Domain Object Model starts to get complex, and create "Test Models" that I specifically use in my SpecFlow scenarios. A Test Model should:

    • Be focused on Business Terminology
    • Allow you to create easy to read Scenarios
    • Provide a layer of decoupling between business terminology and the complex Domain Model

    Let's take a Blog as an example.

    The SpecFlow Scenario: Creating a Blog Post

    Consider the following scenario written so that anyone familiar with how a Blog works knows what's going on:

    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               |
    

    This models a complex relationship, where a Blog has many Blog Posts.

    The Domain Model

    The Domain Model for this Blog application would be this:

    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
    }
    

    Notice that our Scenario has a "Status" with a value of "Working Draft," but the BlogPostStatus enum has WorkingDraft. How do you translate that "natural language" status to an enum? Now enter the Test Model.

    The Test Model: BlogPostRow

    The BlogPostRow class is meant to do a few things:

    1. Translate your SpecFlow table to an object
    2. Update your Domain Model with the given values
    3. Provide a "copy constructor" to seed a BlogPostRow object with values from an existing Domain Model instance so you can compare these objects in SpecFlow

    Code:

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

    It is in the private GetStatus and GetStatusText where the human readable blog post status values are translated to Enums, and vice versa.

    (Disclosure: I know an Enum is not the most complex case, but it is an easy-to-follow case)

    The last piece of the puzzle is the step definitions.

    Using Test Models with your Domain Model in Step Definitions

    Step:

    Given a Blog named "Testing with SpecFlow" exists
    

    Definition:

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

    Step:

    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               |
    

    Definition:

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

    Step:

    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               |
    

    Definition:

    [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 - Some sort of persistent data store whose lifetime is the current scenario)

    Models in a larger context

    Taking a step back, the term "Model" has gotten more complex, and we've just introduced yet another kind of model. Let's see how they all play together:

    • Domain Model: A class modeling what the business wants often being stored in a database, and contains the behavior modeling the business rules.
    • View Model: A presentation-focused version of your Domain Model
    • Data Transfer Object: A bag of data used to transfer data from one layer or component to another (often used with web service calls)
    • Test Model: An object used to represent test data in a manner that makes sense to a business person reading your behavior tests. Translates between the Domain Model and Test Model.

    You can almost think of a Test Model as a View Model for your SpecFlow tests, with the "view" being the Scenario written in Gherkin.

    0 讨论(0)
  • 2020-12-23 18:26

    I would suggest that you try to keep your scenarios as clean as possible, focusing on readability for the non-techie persons in your project. How the complex object graphs are constructed is then handled in your step definitions.

    With that said you still need a way to express hierarchical structures in your specifications, i.e. with Gherkin. As far as I know that is not possible and from this post (in the SpecFlow Google group) it seems that it has been discussed before.

    Basically you could invent a format of your own and parse that in you step. I haven't run into this myself but I think I would try a table with blank values for next level and parse that in the step definition. Like this:

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

    It's not super-pretty i admit but it could work.

    Another way to do it is to use default values and just give the differences. Like this:

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

    In your step definition you then add the "standard" values for the MyObject and fill out the list of children. That approach is a bit more readable if you ask me, but you have to "know" what a standard MyObject is and how that's configured.

    Basically - Gherkin doesn't support it. But you can create a format that you can parse yourself.

    Hope this answer your question...

    0 讨论(0)
提交回复
热议问题