Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpecFlow and complex objects

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 object. A close sample could be this object:

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

Does anyone have any idea how could a write my features/scenarios where MyObject would be instantiated from a "Given" step and used in "When" and "Then" steps?

Thanks in advance

EDIT: Just a shot in mind: are nested tables supported?

like image 453
Ramunas Avatar asked Apr 26 '11 10:04

Ramunas


4 Answers

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

like image 158
stuartf Avatar answered Oct 16 '22 18:10

stuartf


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.

like image 22
Lazydev Avatar answered Oct 16 '22 18:10

Lazydev


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

like image 10
Marcus Hammarberg Avatar answered Oct 16 '22 16:10

Marcus Hammarberg


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.

like image 7
Greg Burghardt Avatar answered Oct 16 '22 16:10

Greg Burghardt