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:
#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 string
s 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.
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.
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
.
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.
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
.
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