Skip to content

Instantly share code, notes, and snippets.

@liammclennan
Last active December 22, 2015 03:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liammclennan/78ea542e2fc120bc643b to your computer and use it in GitHub Desktop.
Save liammclennan/78ea542e2fc120bc643b to your computer and use it in GitHub Desktop.
development workflow

Feature Development Workflow

One of the deliverables of this project is a test plan that is supposed to map what the system does to tests that demonstrate correctness. Because no human being should ever attempt such a task, and because we need tests anyway, I propose to generate the test plan from our automated tests.

Start with a user story

Work should be divided into user stories. I will use my current story for this documentation:

Edit an existing deal leg template (termset)

Add scenarios

We can formalise a story by enumerating a covering set of scenarios:

*When adding a new term*

Given a termset
When a new term is added with a name that is unique within the termset
Then the new term is added to the termset

*When adding a new term with an existing term name*

Given a termset
And the TermSet contains the term "Daily Contract Quantity"
When a new term is added with the name "Daily Contract Quantity"
Then the result is an error
And the new term is not added

From these two scenarios you can see what I mean by 'a covering set' of scenarios. Having established what happens for the duplicate name scenario there is no need for another scenario with a duplicate name. The goal is to have one scenario for each possible eventuality. This story would have more scenarios, but these two are sufficient for demonstration.

Reify scenarios as tests

Convert each scenario to a test. These tests are separated from our other automated tests by being placed in the 'Features' directory and by using the 'ExecuteWithReport' storyq method (which adds the scenario to the generated test plan). Here is the 'When adding a new term with an existing term name' scenario (Features\EditTermSet\WhenAddingATermWithExistingName.cs):

I use a base class to hold the story and other common behaviour:

public class EditTermSetBase
{
    protected Feature EditTermSet = new Story("Edit a TermSet")
        .InOrderTo("affect the generation of deal legs")
        .AsA("middle officer")
        .IWant("to edit an existing term set");
}

then derive a class for the scenario:

[TestFixture]
public class WhenAddingATermWithExistingName : EditTermSetBase
{
    [Test]
    public void Test()
    {
        EditTermSet.WithScenario("when adding a new term with an existing term name")
            .Given(ATermSet)
            .And(TheTermSetContainsTheTerm, "Daily Contract Quantity")
            .When(ANewTermIsAddedWithTheName, "Daily Contract Quantity")
            .Then(TheResultIsAnError)
            .And(TheNewTermIsNotAdded)
            .ExecuteWithReport(MethodBase.GetCurrentMethod());
    }
}

Now the compile errors tell you what methods you need to create. Resharper can generate the method stubs for you. At this point I realise that I need to document how the comparison handles case, so I will create another scenario:

[Test]
public void ExistingTermName_DifferentCase()
{
    EditTermSet.WithScenario("when adding a new term with an existing term name but different case")
        .Given(ATermSet)
        .And(TheTermSetContainsTheTerm, "Daily Contract Quantity")
        .When(ANewTermIsAddedWithTheName, "daily contract quantity")
        .Then(TheResultIsAnError)
        .And(TheNewTermIsNotAdded)
        .ExecuteWithReport(MethodBase.GetCurrentMethod());
}

Note that no extra implementation is required for this scenario. Once we fill out the test methods, and implement the feature the completed test class looks something like:

public class EditTermSetBase
{
    protected Feature EditTermSet = new Story("Edit a TermSet")
        .InOrderTo("affect the generation of deal legs")
        .AsA("middle officer")
        .IWant("to edit an existing term set");
}

[TestFixture]
public class WhenAddingATermWithExistingName : EditTermSetBase
{
    [Test]
    public void ExistingTermName()
    {
        EditTermSet.WithScenario("when adding a new term with an existing term name")
            .Given(ATermSet)
            .And(TheTermSetContainsTheTerm, "Daily Contract Quantity")
            .When(ANewTermIsAddedWithTheName, "Daily Contract Quantity")
            .Then(TheResultIsAnError)
            .And(TheNewTermIsNotAdded)
            .ExecuteWithReport(MethodBase.GetCurrentMethod());
    }

    [Test]
    public void ExistingTermName_DifferentCase()
    {
        EditTermSet.WithScenario("when adding a new term with an existing term name but different case")
            .Given(ATermSet)
            .And(TheTermSetContainsTheTerm, "Daily Contract Quantity")
            .When(ANewTermIsAddedWithTheName, "daily contract quantity")
            .Then(TheResultIsAnError)
            .And(TheNewTermIsNotAdded)
            .ExecuteWithReport(MethodBase.GetCurrentMethod());
    }

    private void ATermSet()
    {
        _termSet = Build.A<TermSet>();
        _termSets.GetWithTerms(_termSet.Id).Returns(_termSet);
    }

    private void TheTermSetContainsTheTerm(string termName)
    {
        _termSet.AddTerm(new Term(termName, "", ""));
    }

    private void ANewTermIsAddedWithTheName(string termName)
    {
        _result = _feature.AddTerm(_termSet.Id, new Term(termName, "", ""));
    }

    private void TheResultIsAnError()
    {
        Assert.AreEqual(Web.Areas.Contracts.Features.EditTermSet.AddTermOutcomes.DuplicateTermName, _result.Outcome);
    }

    private void TheNewTermIsNotAdded()
    {
        Assert.AreEqual(1, _termSet.Terms.Count);
    }

    [SetUp]
    public void BeforeEach()
    {
        _termSets = Substitute.For<ITermSets>();
        _feature = new Web.Areas.Contracts.Features.EditTermSet(_termSets);
    }

    private TermSet _termSet;
    private Web.Areas.Contracts.Features.EditTermSet _feature;
    private Web.Areas.Contracts.Features.EditTermSet.AddTermResult _result;
    private ITermSets _termSets;
}

By using the 'Feature' abstraction, instead of the Controller we avoid having assertions that are expressed in terms of Asp.net mvc types. We don't have to test for 'RedirectToRouteResult's or unpack view models. We also avoid having to mock other dependencies of the controller. The controller code becomes cleaner, and the Feature, Controller and Test all have increased cohesion and decreased coupling. The tradeoff is having to deal with an extra abstraction.

Results

When the code is pushed to the build server the following is added to the 'test plan'

Edit a TermSet
Story is Edit a TermSet  
  In order to affect the generation of deal legs  
  As a middle officer  
  I want to edit an existing term set  
      With scenario when adding a new term with an existing term name  
        Given a term set Passed
          And the term set contains the term(Daily Contract Quantity) Passed
        When a new term is added with the name(Daily Contract Quantity) Passed
        Then the result is an error Passed
          And the new term is not added Passed
      With scenario when adding a new term with an existing term name but different case  
        Given a term set Passed
          And the term set contains the term(Daily Contract Quantity) Passed
        When a new term is added with the name(daily contract quantity) Passed
        Then the result is an error Passed
          And the new term is not added Passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment