Overview
This post describes how to handle scenario level clean-up in a predictable way.
Note: This blog post was originally written for SpecFlow 1.9 version.
In SpecFlow 2+ version you should use scenario scoped bindings with [BeforeScenario] execution order can be now controlled with an Order parameter.
Writing advanced UI tests with SpecFlow and Selenium often requires some test data to be created. During the tests execution this test data is changed. One of requirements for a good test is repeatedness. Test have to be executed multiple times and should complete with the same results.
So we need some sort of clean-up procedures for our tests in order to satisfy above requirement. Let’s consider what SpecFlow could offer us in this case.
Hooks
Hook is the statement of code that is executed after or before particular event. Here are the most useful from my mind:
- [AfterScenario(“MyTag”)] – code is executed after all the scenarios marked with @MyTag.
- [BeforeScenario(“MyTag”)] – code is executed before all the scenarios marked with @MyTag
You could find more info about SpecFlow hooks on the project wiki here.
Let’s consider very simple scenario, where we need some sort of clean-up procedures:
Scenario: Price is updated Given: Price for product foo is 10$ When: Price is changed to 15$ Then: Product foo has price 15$ on the site
What we could expect when test is finished? That the price for product ‘foo’ is 15$.
But what happened when we try to repeat this test? Sure it will fail as now price is 15$. What we need is to organise cleanup action after test execution, so user will be bound to the ABC again.
How could we achieve such behaviour? Well using the hooks in such way.
[AfterScenario] public void AfterScenario() { var title = ScenarioContext.Current.ScenarioInfo.Title; if (title == "Price is updated") { //do some actions to restore previous state } if (title == "SomeOtherTest") { //do some clean-up for that } }
Usually logic inside if statement might be quite complex, as it requires some action to do and reuse the our Page Objects. So AfterScenario method become bloated and hard to read.
Let’s replace above statement with 2 separate methods and mark with our custom attribute:
[ScenarioCleanup("Price is updated")] public void ResetPrice() { //do some actions to reset the price } [ScenarioCleanup("SomeOtherTest")] public void CleanupAfterOtherTest() { //do some other actions }
Code for newly created attribute;
public class ScenarioCleanup : Attribute { public string ScenarioTitle { get; set; } public ScenarioCleanup(string scenarioTitle) { ScenarioTitle = scenarioTitle; } }
And finally the code where all parts are gathered;
[Binding] public class TestBehaviour { public void BeforeTestRun() { _teardownActions = BuildDictionary(this.GetType()); } [AfterScenario()] public void AfterScenario() { _commonTestBehaviour.ResolveScenarioCleanup(ScenarioContext.Current.ScenarioInfo.Title, this); //some other code, like disposing selenium driver or so } [ScenarioCleanup("User changes company")] public void ResetBindedUser() { //do some actions to rebound user to the initial company } public void ResolveScenarioCleanup(string title, object instance) { MethodInfo cleanupMethod; if (_teardownActions.TryGetValue(title, out cleanupMethod)) { cleanupMethod.Invoke(instance, null); } } private Dictionary<string, MethodInfo> _teardownActions; private Dictionary<string, MethodInfo> BuildDictionary(Type type) { var withAttrs = type.GetMethods() .Where(m => m.GetCustomAttributes(typeof(ScenarioCleanup), false).Length > 0) .ToDictionary(key => ((ScenarioCleanup[])key.GetCustomAttributes(typeof(ScenarioCleanup), false))[0].ScenarioTitle, val => val); return withAttrs; } }
In short:
- Before TestRun via reflection all the cleanup methods init.
- When scenario is executed, AfterScenario() method runs
- In AfterScenario method, corresponding clean-up method is resolved and executed