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:
- Translate your SpecFlow table to an object
- Update your Domain Model with the given values
- 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.