SpecFlow and complex objects

前端 未结 7 980
旧巷少年郎
旧巷少年郎 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: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   | 

    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 |

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

    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   | 

    This is not so hard.

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

    This is not so hard.

    | | 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(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.

提交回复
热议问题