Acceptance Test Driven Development Using Specflow in the .NET Applications

Difference between TDD  and ATDD

I am not providing an extensive description of the differences between TDD and ATDD, surely we can get as much about in the web as we need. I will tell you briefly of the ATDD advantages over TDD, how testers in my team will contribute Unit Tests by just copying from the test documents and these can also be easily understood by the customers and a tool like Specflow. Finally generate test class from the spec flow feature file generated by Test team and handover to the development team for further changes.

This really saves much time for development teams that currently hate to write UnitTests in regular projects. That really is a time-consuming job. But the following approach for UnitTests are clearly understood by customers also and testers are helping to generate the feature class (Spec flow) directly copying the test cases from the test script.

The following is a sample user story as shared by my product owner or customer:

My project user story: As a security measure, I want my IT system software to print the amount in words when dispensing a cheque using the payroll system.

(This requirement will convert rupees in words against the amount number written on the cheque.)

The sample added Specflow test feature file is as given below (ChequeManagerAcceptanceTest.feature). Testers can add multiple test cases in the similar format in the feature file. Finally the Specflow tool generated unit test project is based on the test scenarios and handover to the development team. Observe the code file attachment to get more details.

Feature: ChequeManagerAcceptanceTest

As a feature

I want the system to print out the amount in words along with digits in a cheque.

Scenario: Converting positive whole numbers to words up to 99 crore.

Assuming I input the following numbers from the table below:

table

When I press print cheque from the system, then the cheque should have the corresponding word representation from the table.

In short, the problem with a TDD approach is that all the tested behaviors are related to the code and not the business requirements or real-world scenarios. SpecFlow fixes that by using a Gherkin parser, allowing you to write test scenarios using a business-readable, domain-specific language instead of code (UnitTest Projects).

TDD approach

TDD approach

Acceptance Test Driven Development (ATDD approach)

 

List of ATDD Tools

Out of the following available tools for ATDD development, Specflow exactly fits into .NET based applications:

  • Cucumber
  • RSpec
  • Behat
  • SpecFlow
  • JBehave
  • Lettuce
  • easyb

Demo using Specflow:

My User Acceptance story

My project user story: As a security measure, I want my IT system software to print the amount in words when dispensing a cheque using the payroll system.

Write the following code in the Cheque Manager Application (Cheque.cs):

  1. using System;  
  2.   
  3. using System.Collections.Generic;  
  4.   
  5. using System.Linq;  
  6.   
  7. using System.Text;  
  8.   
  9. namespace ChequeManagerApp  
  10. {  
  11.   
  12.     public class Cheque  
  13.     {  
  14.   
  15.         enum SingleDigit  
  16.         {  
  17.             one = 1,  
  18.             two,  
  19.             three,  
  20.             four,  
  21.             five,  
  22.             six,  
  23.             seven,  
  24.             eight,  
  25.             nine  
  26.         }  
  27.   
  28.         enum SpecialTwoDigits  
  29.         {  
  30.             eleven = 11,  
  31.             twelve = 12,  
  32.             thirteen = 13,  
  33.             fourteen = 14,  
  34.             fifteen = 15,  
  35.             sixteen = 16,  
  36.             seventeen = 17,  
  37.             eighteen = 18,  
  38.             nineteen = 19  
  39.         }  
  40.   
  41.         enum Tens  
  42.         {  
  43.             ten = 1,  
  44.             twenty = 2,  
  45.             thirty = 3,  
  46.             forty = 4,  
  47.             fifty = 5,  
  48.             sixty = 6,  
  49.             seventy = 7,  
  50.             eighty = 8,  
  51.             ninety = 9  
  52.         }  
  53.   
  54.         enum BaseNumber  
  55.         {  
  56.             crore = 10000000,  
  57.             lakh = 100000,  
  58.             thousand = 1000,  
  59.             hundred = 100  
  60.   
  61.         }  
  62.   
  63.         /// <summary>  
  64.         /// Returns english word representation of positive whole numbers  
  65.         /// up to 99 crores  
  66.         /// </summary>  
  67.         /// <param name="number">number to represent</param>  
  68.         /// <returns>Word representation</returns>  
  69.   
  70.         public string GetAmountinWords(int number)  
  71.         {  
  72.             StringBuilder main = new StringBuilder();  
  73.             //Starting base as crore  
  74.             int baseNbr = 10000000;  
  75.             HandleForBase(number, main, baseNbr);  
  76.             return main.ToString().Trim();  
  77.         }  
  78.   
  79.         private string PrintNumberWithRespectTo(Type en, int number)  
  80.         {  
  81.             foreach(var enumOption in Enum.GetValues(en))  
  82.             {  
  83.                 if (number == (int) enumOption)  
  84.                 {  
  85.                     return enumOption.ToString();  
  86.                 }  
  87.             }  
  88.             return "Invalid number for selected type";  
  89.         }  
  90.   
  91.         private string PrintRegularTwoDigits(int nbr)  
  92.         {  
  93.             StringBuilder sb = new StringBuilder();  
  94.             var tens = nbr / 10;  
  95.             if (tens > 0)  
  96.             {  
  97.                 sb.Append(PrintNumberWithRespectTo(typeof(Tens), tens) + " ");  
  98.                 nbr = nbr - (tens * 10);  
  99.             }  
  100.             if (nbr > 0)  
  101.             {  
  102.                 sb.Append(PrintNumberWithRespectTo(typeof(SingleDigit), nbr) + " ");  
  103.             }  
  104.   
  105.             return sb.ToString();  
  106.         }  
  107.   
  108.         private int FindNextBase(int baseNbr)  
  109.         {  
  110.             while (PrintNumberWithRespectTo(typeof(BaseNumber), (baseNbr / 10)) == "Invalid number for selected type")  
  111.             {  
  112.                 if (baseNbr < 100)  
  113.                 return 0;  
  114.                 baseNbr = baseNbr / 10;  
  115.             }  
  116.             return baseNbr / 10;  
  117.         }  
  118.   
  119.         private void HandleForBase(int number, StringBuilder main, int baseNbr)  
  120.         {  
  121.             var baseRepresentation = PrintNumberWithRespectTo(typeof(BaseNumber), baseNbr);  
  122.             var baseDigit = number / baseNbr;  
  123.             if (baseDigit > 0)  
  124.             {  
  125.                 HandleDigits(baseDigit, main);  
  126.                 main.Append(" " + baseRepresentation + " ");  
  127.                 number = number - (baseDigit * baseNbr);  
  128.             }  
  129.             if (number > 0)  
  130.             {  
  131.                 baseNbr = FindNextBase(baseNbr);  
  132.                 if (baseNbr == 0)  
  133.                 {  
  134.                     HandleDigits(number, main);  
  135.                     number = 0;  
  136.                     return;  
  137.                 }  
  138.   
  139.                 HandleForBase(number, main, baseNbr);  
  140.             }  
  141.             return;  
  142.         }  
  143.   
  144.         private void HandleDigits(int number, StringBuilder main)  
  145.         {  
  146.             if (number <= 9)  
  147.             {  
  148.                 main.Append(PrintNumberWithRespectTo(typeof(SingleDigit), number));  
  149.   
  150.             }   
  151.             else if (number <= 19 && number != 10)  
  152.             {  
  153.                 main.Append(PrintNumberWithRespectTo(typeof(SpecialTwoDigits), number));  
  154.   
  155.             }  
  156.             else  
  157.             {  
  158.                 main.Append(PrintRegularTwoDigits(number).Trim());  
  159.   
  160.             }  
  161.         }  
  162.     }  
  163. }

How to Include Specflow in .NET projects

Install Specflow from the NugetPackage Manager:

  1. Create a new Test Project.
  2. Add a reference to the project you want to test in the new Test Project. (Note: This step only application you installed Specflow manual apart from NuGet ).
  3. Add a reference to %USERPROFILE%\AppData\Local\Microsoft\VisualStudio\10.0\Extensions\TechTalk\SpecFlow\1.9.0\TechTalk.SpecFlow.dll to the new Test Project.
  4. Change the testing framework. Add a new Application Configuration file to the Test Project and add the following:
    1. <?xml version="1.0" encoding="utf-8" ?>  
    2. <configuration>  
    3.    <configSections>  
    4.       <section name="specFlow" type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow"/>  
    5.    </configSections>  
    6.    <specFlow>  
    7.       <unitTestProvider name="MsTest" />  
    8.    </specFlow>  
    9. </configuration> 
    This will use the Microsoft Testing Framework. (Note: If you are using NUNIT then change the UnitTest Provider).
  5. Add a new item to the Test Project and select SpecFlow Feature File.
  6. Write test cases on the new feature file.
  7. Right-click on the definition and select Generate Step Definitions. This will create a .cs file with a skeleton for the test definition. The developer must edit that file to implement the tests.

User Story against the following specflow feature class

My project user story: As a security measure, I want my IT system software to print the amount in words when dispensing cheque using payroll system.

ChequeManagerAcceptanceTest.feature – Added Specflow test feature file

Feature: ChequeManagerAcceptanceTest

As a feature I want the system to print out the amount in words along with digits in a cheque.

Scenario: Converting positive whole numbers to words up to 99 crore.

Given: I will input following numbers from the table below:

table

When I press print cheque from the syste.

Then the cheque should have corresponding word representation from the table Generate:

  1. using System;  
  2. using TechTalk.SpecFlow;  
  3. using System.Collections;  
  4. using ChequeManagerApp;  
  5. using Microsoft.VisualStudio.TestTools.UnitTesting;  
  6.   
  7. namespace ChequeManagerUnitTest  
  8. {  
  9.     [Binding]  
  10.   
  11.     public class ChequeManagerAcceptanceTestSteps  
  12.     {  
  13.         [Given(@  
  14.         "I input following numbers from the table below")]  
  15.   
  16.         public void GivenIInputFollowingNumbersFromTheTableBelow(Table table)  
  17.         {  
  18.             ScenarioContext.Current.Add("inputtable", table);  
  19.         }  
  20.   
  21.         [When(@  
  22.         "I press print cheque from the system")]  
  23.   
  24.         public void WhenIPressPrintChequeFromTheSystem()  
  25.         {  
  26.             var inputtable = ScenarioContext.Current["inputtable"as Table;  
  27.             var cheque = new Cheque();  
  28.             Hashtable output = new Hashtable();  
  29.             foreach(TableRow input in inputtable.Rows)  
  30.             {  
  31.                 output.Add(input[0], cheque.GetAmountinWords(int.Parse(input[0])));  
  32.             }  
  33.   
  34.             ScenarioContext.Current.Add("outputtable", output);  
  35.         }  
  36.   
  37.         [Then(@  
  38.         "the cheque should have corresponding word representation from the table")]  
  39.   
  40.         public void ThenTheChequeShouldHaveCorrespondingWordRepresentationFromTheTable()  
  41.         {  
  42.             var inputtable = ScenarioContext.Current["inputtable"as Table;  
  43.             var outputtable = ScenarioContext.Current["outputtable"as Hashtable;  
  44.             foreach(TableRow input in inputtable.Rows)  
  45.             {  
  46.                 Assert.AreEqual(input[1], outputtable[input[0]]);  
  47.             }  
  48.         }  
  49.     }  

Implemented Unit Tests

  1. using System;  
  2. using System.Text;  
  3. using System.Collections.Generic;  
  4. using System.Linq;  
  5. using Microsoft.VisualStudio.TestTools.UnitTesting;  
  6. using ChequeManagerApp;  
  7.   
  8. namespace ChequeManagerUnitTest.UnitTest  
  9. {  
  10.     /// <summary>  
  11.     /// Summary description for ChequeManagerUnitTest  
  12.     /// </summary>  
  13.   
  14.     [TestClass]  
  15.     public class ChequeManagerUnitTest  
  16.     {  
  17.         public TestContext TestContext   
  18.         {  
  19.             get;  
  20.             set;  
  21.         }  
  22.   
  23.         [TestMethod]  
  24.         [DeploymentItem("D:\\Agile\\ChequeManager\\ChequeManagerUnitTest\\UnitTest\\SingleDigits.xml")]  
  25.         [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",  
  26.             "|DataDirectory|\\UnitTest\\SingleDigits.xml",  
  27.             "Row",  
  28.         DataAccessMethod.Sequential)]  
  29.         public void Should_Return_Valid_Word_For_Single_Digit_Numbers()  
  30.         {  
  31.             string expected;  
  32.             string actual;  
  33.             TestChequePrintMethod(out expected, out actual);  
  34.             Assert.IsTrue(expected.Equals(actual, StringComparison.CurrentCultureIgnoreCase),  
  35.             String.Format("Test failed , expceted value '{0}'. Actual is'{1}'", expected, actual));  
  36.         }  
  37.         private void TestChequePrintMethod(out string expected, out string actual)  
  38.         {  
  39.             int number = int.Parse((string) TestContext.DataRow["number"]);  
  40.             expected = TestContext.DataRow["output"].ToString();  
  41.             var sut = new Cheque();  
  42.             actual = sut.GetAmountinWords(number);  
  43.         }  
  44.         [TestMethod]  
  45.         [DeploymentItem("D:\\Agile\\ChequeManager\\ChequeManagerTest\\UnitTest\\10To20.xml")]  
  46.         [DataSource("Microsoft.VisualStudio.TestTools.DataSource.XML",  
  47.             "|DataDirectory|\\UnitTest\\10To20.xml",  
  48.             "Row",  
  49.         DataAccessMethod.Sequential)]  
  50.         public void Should_Return_Valid_Word_For_Below_20_Numbers()  
  51.         {  
  52.             string expected;  
  53.             string actual;  
  54.             TestChequePrintMethod(out expected, out actual);  
  55.             Assert.IsTrue(expected.Equals(actual, StringComparison.CurrentCultureIgnoreCase),  
  56.             String.Format("Test failed , expceted value '{0}'. Actual is'{1}'", expected, actual));  
  57.         }  
  58.   
  59.         [TestMethod]  
  60.         public void Should_Return_Valid_Word_For_Crore_Numbers()  
  61.         {  
  62.             var cheque = new Cheque();  
  63.             Assert.AreEqual("one crore", cheque.GetAmountinWords(10000000).Trim());  
  64.             Assert.AreEqual("nineteen crore", cheque.GetAmountinWords(190000000).Trim());  
  65.             Assert.AreEqual("twenty two crore", cheque.GetAmountinWords(220000000).Trim());  
  66.             Assert.AreEqual("ninety nine crore", cheque.GetAmountinWords(990000000).Trim());  
  67.         }  
  68.   
  69.         [TestMethod]  
  70.         public void Should_Return_Valid_Words_For_Lakhs_Number()  
  71.         {  
  72.             var cheque = new Cheque();  
  73.             Assert.AreEqual("one crore nine lakh", cheque.GetAmountinWords(10900000).Trim());  
  74.             Assert.AreEqual("one crore eighteen lakh", cheque.GetAmountinWords(11800000).Trim());  
  75.             Assert.AreEqual("one crore ninety nine lakh", cheque.GetAmountinWords(19900000).Trim());  
  76.             Assert.AreEqual("one crore ninety nine lakh ten thousand twenty three",  
  77.             cheque.GetAmountinWords(19910023).Trim());  
  78.             Assert.AreEqual("ninety nine lakh", cheque.GetAmountinWords(9900000).Trim());  
  79.         }  
  80.   
  81.         [TestMethod]  
  82.         public void Should_Return_Valid_Words_For_Regular_Two_Digit_Number()  
  83.         {  
  84.             var cheque = new Cheque();  
  85.             Assert.AreEqual("twenty three", cheque.GetAmountinWords(23).Trim());  
  86.             Assert.AreEqual("fifty seven", cheque.GetAmountinWords(57).Trim());  
  87.         }  
  88.   
  89.         [TestMethod]  
  90.         public void Should_Return_Valid_Words_For_Thousand_Digit_Number()  
  91.         {  
  92.             var cheque = new Cheque();  
  93.             Assert.AreEqual("one thousand twenty three", cheque.GetAmountinWords(1023).Trim());  
  94.             Assert.AreEqual("nine thousand one hundred fifty seven", cheque.GetAmountinWords(9157).Trim());  
  95.         }  
  96.     }  

Note: I will cover in detail in the next article how to run our test projects command line options in an automated way using Specflow test runner for test providers NUnit and MSTest.

The following shows the generated HTML report page after running the tests using NUNIT test runner.

Next Recommended Readings