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:

  1. South Colorado November 12 Meeting