South Colorado .NET User Group - SpecFlow
For the South Colorado .NET User Group meeting, we had Rudy Lacovara speak on SpecFlow. SpecFlow follows much of the Behavior Driven Development model. Other examples of this in the wild are Cucumber/Gerkin in the Ruby community.
SpecFlow allows developers to write automated tests in a "natural language that can be understood and written by anyone".
To install SpecFlow, you need the NuGet package and the extension manager. (See the installation documentation)
SpecFlow creates integration tests. Basically, you are executing parts of the system that run in some part of the environment. Hitting the database and saving real data. Getting and cleaning up real data.
Below is an example Feature File (Gerkin)
@Company
Feature: CompanySave
Scenario: Anonymous user submits a valid company
Given user is not logged in
And they create a company with name, address and description
When they save the company
Then the company is saved to the database
And the saved company has a status of PendingReview
Scenario: Authenticated jobseeker submits a valid company
Given user is authenticated jobseeker
And they create a company with name, address and description
When they save the company
Then the company is saved to the database
And the saved company has a status of PendingReview
Scenario: Authenticated employer submits a valid company
Given user is authenticated employer
And they create a company with name, address and description
When they save the company
Then the company is saved to the database
And the saved company has a status of PendingReview
Scenario: Authenticated employer requires Address1
Given user is Authenticated Employer
When they create a company with blank data
Then they run validation for the company
And a validation error is thrown for field "CompanyAddress1"
Scenario: Authenticated employer requires City
Given user is Authenticated Employer
When they create a company with blank data
Then they run validation for the company
And a validation error is thrown for field "CompanyCity"
Scenario: Authenticated employer requires State
Given user is Authenticated Employer
When they create a company with blank data
Then they run validation for the company
And a validation error is thrown for field "CompanyState"
You can also use a table to reduce the number of scenarios written for validation. Above three scenarios were written for Company Address, City and State.
Scenario Outline: SubmitCompany validation requires field - Outline
Given user is Authenticated Employer
And then create a company with blank data
When they run validation for the company
Then a valadation error is thrown for Field "<fieldName>"
Example:
|fieldName |
|CompanyAddress1 |
|CompanyAddress2 |
|CompanyCity |
|CompanyState |
|CompanyName |
Next, we generate a Definition File which will contain our C# code for our steps in our scenarios.
To do this, you can use "Generate Step Definitions" to stub out the steps in the feature. Part of this process to generate the required methods uses regular expressions to wire up these definitions from the feature file.
Rudy uses a TestHelper class to better manage setting up a company for the tests. He also recommends using an AuthTokenClass for credential management.
public class CompanySaveSteps {
private dynamic context = new ExpandoObject();
private CompanyService companyService = new CompanyService();
[Given(@"user is not logged in")]
public void GivenUserIsNotLoggedIn() {
context.AuthToken = TestHelper.GetAnonymousJobseekerAuthToken();
ScenarioContext.Current.Pending();
}
[Given(@"they create a company with name, address and description")]
public void GivenTheyCreatedCompanyWithNameAddressAndDescription()
{
var company = TestHelper.GetTestCompany();
company.CompanyAddress1 = "1600 Pennsylvania Ave";
company.Description = "My description";
context.Company = company;
}
[Given(@"user is authenticated jobseeker")]
public void GivenUserIsAuthenticatedJobseeker()
{
context.AuthToken = TestHelper.GetAuthenticatedJobseekerAuthToken();
}
[Given(@"user is authenticated employer")]
public void GivenUserIsAuthenticatedEmployer()
{
context.AuthToken = TestHelper.GetAuthenticatedEmployerAuthToken();
}
[When(@"they save the company")]
public void WhenTheySaveTheCompany
{
companyService.Save(context.Company, context.AuthToken);
}
[When(@"they create a company with blank data")]
public void WhenTheyCreateACompanyWithBlankData()
{
}
[BeforeScenario]
public void BeforeScenario()
{
companyService.Delete(TestHelper.GetTestCompany(), TestHelper.GetAdminAuthToken());
}
[Then(@"the company is saved to the database")]
public void ThenCompanyIsSavedToTheDatabase()
{
context.CheckComany = companyService.Getcompany(context.Compnay.CompanyGuid);
Assert.IsNotNull(context.CheckCompany, "There's no company man");
}
[Then(@"The saved company has a status of pending review)]
public void ThenTheSavedCompanyHasAStatusOfPendingReview() {
Assert.IsTrue(context.CheckCompany.Status == CompanyStatus.PendingReview, "wrong status man!");
}
[When(@"They run validation for the compnay]
public void WhenTheyRunValidationForTheCompany()
{
var validator = new CompnayValidator();
try {
validator.Validate(context.Company);
}
catch(RuleException ex)
{
context.RuleException = ex;
}
}
[Then(@"a validation error is thrown for field ""(.*)"""]
public void ThenValidationErrorIsThrownForField(string fieldName)
{
var ex = context.RuleException as RuleException
Assert.IsTrue(ex.ErrorList.FirstOrDefault(m => m.FieldName == fieldName)!=null, "no rule exception found");
}
If you notice in the code sample above each method relates to a step in the scenarios. You will also we have simple tear down and setup per run. BeforeScenario method allows us to clean out the company.
References: