Specflow Tips and Tricks
I have spent a lot of time over the last few years learning to improve code in large legacy products, and, for me, a large part of improving the code has involved writing regression tests. Regression tests are tests written before refactoring and used to ensure behavior hasn't regressed after refactoring.
Specflow is one of the best tools I have found to help write this kind of test in a web application. Part of the reason Specflow is so useful for this purpose is that it effectively gives you a "second interface" (the steps) into using (and automating) your product, and this second interface helps to make the resulting tests (features/specs) more durable to changes made during refactoring. It can be a great tool for a flow that involves writing tests to understand how a system currently works, refactoring the system to a more maintainable structure, and then re-testing to ensure feature parity between the new and old code.
Specflow is an incredible tool, but it comes with a steep learning curve. It has few opinions about how things should be done. While it does provide some constructs, it's up to you to design a library that makes sense for your particular business domain. While Specflow does provide some sample projects and robust documentation, I personally found it difficult to bridge the gap between the toy examples and the more serious projects, and so have many people I've spoken with. In this article, I'd like to share some of the small things that I started doing that have made me more productive with Specflow.
Before we get into the specific advice, it's important to state some assumptions. I am assuming that you have already done the getting started guide and that you already understand what scenarios , steps , step argument transformations , and context injection are and how they work generally.
Tip #1: Use Dictionaries as Contexts
This tip might seem obvious to some, but it wasn't to me. If you want to pass information between steps, you should obviously use some kind of a context, so why not a list? One reason is that you will constantly need to search that list throughout your tests. You can simplify your life and optimize your code (at least slightly) by storing them in a dictionary with a key you can readily reference (like a name that you can easily write as an input to your steps.)
Also, if your objects don't have an obvious "name" type property that you could consider unique, I have found that using dictionaries in my context makes it easy to make up an identifier (in my feature files) if I need one.
It also makes checking to see if you have something in context trivial. If you wanted to, for example, write a StepArgumentTransformation
that translates from a task's "name" (as a string) to the task object you previously set up, you could easily do something like this context:
public class TaskContext: Dictionary<string, Task> { }
This allows you to easily write transformations like this:
[StepArgumentTransformation]
public Task StringToTask(string name) {
// Note, _context is of type TaskContext
if(!_context.ContainsKey(name)) {
throw new NotFoundException($"Could not find a task with name {name}, did you previously set one up?");
}
return _context[name];
}
Now it's easy to write steps like this one:
[When("I complete task (.+)")]
public void CompleteTask(Task task) {
task.Status = TaskStatus.Complete;
}
This step will "automatically" pull the previous object out of context. Even better, now the process of referencing a previously initialized task has been simplified to taking the Task type as a parameter to your step function, so future steps (like one asserting that a specific step has been completed) are easier to write.
Tip #2: Use Contexts to House Reusable Logic
Plain data contexts are a beautiful, but contexts can do so much more. If you have complex logic that lots of different steps might need to use, it's generally better to write that logic into a context than it is to write steps that call out to each other . Generally, it's a good idea to keep data storage separate from data manipulation, so don't just add methods to your dictionary contexts. Luckily, Specflow's DI system is clever enough to let you inject contexts into other contexts, so you can easily make a data-only context that stores data and inject it into a more complex context (or several more complex contexts) to share data easily.
Tip #3: Don't Pass Tables to Steps
This one might seem a little odd at first, but stick with me. Tables are an internal Gherkin (Specflow) type, and they will, in all but the simplest cases, require some pre-processing before you can do anything useful with them.
That pre-processing shouldn't be in your steps. Steps are a place to put business logic—or at least orchestrate the execution of business logic. Transforming a table into a more useful type isn't really part of that type.
Instead, pre-process table types in your step argument transformations, and provide a domain object to your steps that they can manipulate. Doing so will make it a lot easier to share code between object initialization and object checking, because you can use the same transformations to result in objects either to save or to compare against!
Tip #4: You Can Chain Step Argument Transformations
There isn't much to say about this one other than the title. If you have a complex data type, but it's easy to identify based on another type, consider chaining the step argument transformations. Consider the following example:
[StepArgumentTransformation]
public Automobile Transform(string Vin) {
return _automobileContext[Vin];
}
[StepArgumentTransformation]
public Truck Transform(Automobile vehicle) {
return _truckContext[vehicle.Identifier];
}
This is an arbitrary example, but I think you get the idea. The obvious key for the truck context isn't the Vin, but that's a more intuitive identifier for us to use in our Scenarios
. If we then wrote a step that took a Truck
as a parameter, but provided a string of an Automobile
Vin, Specflow is smart enough to chain the two transformations together.
Tip #5: Don't Query Where You Assert
It should go without saying, but I have found that folks often find the separation of one "logical operation" into multiple steps unintuitive. I have seen developers write steps like, "When I query the object, it should have status Finished". This seems fine—until you have to write your next step asserting something else. And if you need to assert about two different fields in the same test, two round trips to the database have to be made!
To make it easier to reuse your steps, always separate querying from asserting. You should have a step to retrieve your data; then, store it in a context. That way, you can easily write as many steps asserting as many things about it as you want without creating more round trips to your database.
Tip #6: Use Tables to Make Assertions
Thinking back to Tip #3, if you made a nice step argument transformation to turn your table into an object, you can easily pass that same object into a step that is trying to assert that the properties filled out in the object match! Doing so allows you to assert about multiple properties on an object in one step if that is what your test needs.
For example, if I had this type:
public class MyType {
public int SomeNumber;
public string MyFavoriteColor;
}
and I had the following transformation:
public IList<MyType> TableToCustomType(Table table){
return table.CreateSet<MyType>();
}
Then I could easily write a step to initialize a new set of MyType, but I could also write this:
[Then("The row named (.+) should match")]
public void SavedRecordShouldMatch(string name, IList<MyType matchingObject) {
var rowToCompareTo = matchingObject.First();
if(_context[name].SomeNumber != rowToCompareTo.SomeNumber) {
throw new Exception();
}
if(_context[name].MyFavoriteColor != rowToCompareTo.MyFavoriteColor) {
throw new Exception
}
}
Tip #7: Use Scoped Step Definitions
Scoped step definitions are something I wish I'd known about sooner. They let you scope steps to only be available to features that contain certain tags. This makes it a lot easier to maintain a large step library in a monolithic code base. You can easily lock steps that have to do with a certain domain in your code base behind a certain tag. Doing so has two benefits: first, It's less likely someone will use steps they probably shouldn't. Second, your feature files now naturally have domain-specific (or even functionality-specific) tags on them, so if you are making changes in a certain domain, it is easy to see what tests you should re-run. All without involving complex and CPU-intensive static code analysis!
Specflow is an incredibly useful tool, but it can be a challenge to learn and get comfortable with. Hopefully these tips help you make the transition from understanding the basic concepts to applying them in useful ways.