Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / DevOps / testing

A Slice of Cucumber

5.00/5 (1 vote)
9 Oct 2019CPOL2 min read 20K   112  
Visual C++ tests are created from Gherkin DSL feature files using a Python 2.7 script

Update

The code from this article has been generalised to other test frameworks in other languages and is packaged for download from PyPI.

Introduction

Taking an existing project that uses the Visual Studio CppUnitTestFramework and introducing BDD tests written in the Gherkin DSL would mean switching test frameworks and adding other dependencies if Cucumber-CPP were used. To keep things simple, a Python 2.7 script was written to parse (English) Gherkin feature files and produce stub code for CppUnitTestFramework.

This article presents an example of using the attached script and header file. It is left to the readers discretion to modify the script to produce output for their favoured test framework in their working language.

The Tests

The file "example.feature" contains the tests written in Gherkin.

Feature: Accumulator

Background:
  Given an initial <value>

Scenario Outline: Add one other
  When you add a <second>
  Then the result is <sum>
  Examples:
    | value | second | sum |
    | 1     | 2      | 3   |
    | 2     | 2      | 4   |

Scenario Outline: Add two others
  When you add a <second>
  And you add a <third>
  Then the result is <sum>
  Examples:
    | value | second | third  | sum |
    | 1     | 2      | 3      | 6   |
    | 2     | 3      | 4      | 9   |

The Stub Code

Running the script "features.py" produces the C++ code:

C++
#include "stdafx.h"
#include "CppUnitTest.h"

#include "TestUtils/LogStream.h"

#include <iostream>
#include <memory>

using namespace Microsoft::VisualStudio::CppUnitTestFramework;

#define AddOneOtherInst(_VALUE, _SECOND, _SUM) \
    TEST_METHOD(AddOneOther ## _VALUE ## _SECOND ## _SUM) \
    { \
        AddOneOther(#_VALUE, #_SECOND, #_SUM); \
    }

#define AddTwoOthersInst(_VALUE, _SECOND, _THIRD, _SUM) \
    TEST_METHOD(AddTwoOthers ## _VALUE ## _SECOND ## _THIRD ## _SUM) \
    { \
        AddTwoOthers(#_VALUE, #_SECOND, #_THIRD, #_SUM); \
    }

where the two scenario outlines are represented as macros. Note that the arguments are passed through as strings which may not be the desired behaviour. The test method name is formed by concatenation, in this case "## _SUM" is superfluous, in other cases characters such as periods may be passed through causing compile errors. Also, as no separator character is concatenated, the test name may not necessarily be unique, i.e., it may be best to edit the generated name.

C++
namespace Example
{
    TEST_CLASS(Accumulator)
    {
        static std::streambuf* oldBuffer;
        static std::shared_ptr<std::streambuf> newBuffer;

The namespace is just the file name minus its extension, the class name is the feature.

C++
void GivenAnInitial(std::string value)
{
    std::clog << "      Given an initial " << value << std::endl;
}

void WhenYouAddA(std::string second)
{
    std::clog << "      When you add a " << second << std::endl;
}

void ThenTheResultIs(std::string sum)
{
    std::clog << "      Then the result is " << sum << std::endl;
}

The steps of the scenarios are implemented as individual methods. To implement the actual tests, a class variable would have to be declared to represent the sum, initialized by the first method, incremented by the second method and tested in the third. Obviously, the arguments would be better if they were changed to be of type int.

C++
static void AddOneOther(std::string value, std::string second, std::string sum)
{
    std::clog << "  Feature: Accumulator" << std::endl;
    std::clog << "    Scenario: Add one other" << std::endl;
    Accumulator instance;
    instance.GivenAnInitial(value);
    instance.WhenYouAddA(second);
    instance.ThenTheResultIs(sum);
}

static void AddTwoOthers(std::string value, std::string second, std::string third, std::string sum)
{
    std::clog << "  Feature: Accumulator" << std::endl;
    std::clog << "    Scenario: Add two others" << std::endl;
    Accumulator instance;
    instance.GivenAnInitial(value);
    instance.WhenYouAddA(second);
    instance.WhenYouAddA(third);
    instance.ThenTheResultIs(sum);
}

The scenario outlines are implemented as static functions to be called by their corresponding macros.

C++
TEST_CLASS_INITIALIZE(ClassInitialize)
{
    newBuffer = std::make_shared<TestUtils::LogStream>();
    oldBuffer = std::clog.rdbuf(newBuffer.get());
    std::clog << "Entering Example" << std::endl;
}

TEST_CLASS_CLEANUP(ClassCleanup)
{
    std::clog << "Exiting Example" << std::endl;
    std::clog.rdbuf(oldBuffer);
    newBuffer = nullptr;
}

The Visual Studio Logger class is wrapped in a stream and that stream temporarily displaces the one used by std::clog.

C++
    public:

        AddOneOtherInst(1, 2, 3);

        AddOneOtherInst(2, 2, 4);

        AddTwoOthersInst(1, 2, 3, 6);

        AddTwoOthersInst(2, 3, 4, 9);
    };

    std::streambuf* Accumulator::oldBuffer = nullptr;
    std::shared_ptr<std::streambuf> Accumulator::newBuffer = nullptr;
}

The four tests are implemented as instances of the macros.

Output

Build and run produces the output:

------ Run test started ------
Entering Example
  Feature: Accumulator
    Scenario: Add two others
      Given an initial 2
      When you add a 3
      When you add a 4
      Then the result is 9
  Feature: Accumulator
    Scenario: Add two others
      Given an initial 1
      When you add a 2
      When you add a 3
      Then the result is 6
  Feature: Accumulator
    Scenario: Add one other
      Given an initial 2
      When you add a 2
      Then the result is 4
  Feature: Accumulator
    Scenario: Add one other
      Given an initial 1
      When you add a 2
      Then the result is 3
Exiting Example
========== Run test finished: 4 run (0:00:00.6816458) ==========

from tests named:

  • AddOneOther123
  • AddOneOther224
  • AddTwoOthers1236
  • AddTwoOthers2349

Points of Interest

Prior to implementing the script, my test classes only had static methods and exhibited the use of copy and paste. Now, my test classes inherit so that common steps are written in base classes, i.e., my test code looks more like what it tests.

History

  • 2016/03/09: First release
  • 2019/10/09: Added link to Cornichon at PyPI

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)