[C++] Start Using Cucumber
Edit Feb. 2024: Cucumber Part 2
There is a follow up article, Start Using Cucucmber Part 2. This Time With CWT-Cucumber, which might interest you. In this article we use CWT-Cucumber, which is a Cucumber interpreter implemented in C++.
Find this example here on GitHub.
With Cucumber you can start behavior driven development (BDD) in your projects. The C++ version is available on the official GitHub repository.
Cucumber allows you to write tests in a given-when-then style
, which are readable for basically everyone. For this article I'll use a bookstore API, where we can:
- Add books
- Remove books
- Get a specific book
- Get books count
Where your test then look like the following. Each Scenario represents one test case then.
Scenario: Find added book
Given An empty bookstore
When Add "a book" by "somebody" with id 123
Then Bookstore has "a book" by "somebody"
Scenario: Add books to an empty bookstore
Given An empty bookstore
When Add "a book" by "somebody" with id 123
And Add "another book" by "another author" with id 456
Then Total books count is 2
Scenario: Find added book
Given An empty bookstore
When Add "a book" by "somebody" with id 123
Then Bookstore has "a book" by "somebody"
Scenario: Add books to an empty bookstore
Given An empty bookstore
When Add "a book" by "somebody" with id 123
And Add "another book" by "another author" with id 456
Then Total books count is 2
And this is pretty much readable and tells you something about your API. You can initialize an empty bookstore, add books by name
, author
and an id
and finally you can verify on your API that a book exists and how many.
Prerequisites
Find the installation guide in the GitHub Readme.
We need to install following dependencies on our machine to run cucumber:
- Cucumber (to execute cucumber)
- Cucumber-cpp (to link our executable)
- Boost
- Googletest (works also with other testframeworks, we'll use googletest)
If you don't have Boost as dependency in a current project, or you running on Windows (because this can quite an effort to install all this), my best practice is, use Docker. Install all this tools in a Ubuntu Docker and run your tests from there.
How Does Cucumber Work?
After installing all dependencies and building cucumber-cpp from GitHub, you are ready to compile your test/cucumber executable. You compile all defined steps, which you support into this application. Just like in the code snippet above.
Execute this application and it'll wait until we start the tests with the cucumber command line tool. This parses all tests from a *.feature
file and passes them to your executable.
The results are then printed to the terminal and you can indicate their status:
Let's Implement Step Definitions
Cucumber provides macros to implement steps to your C++ project. Let's go ahead and implement one of the Scenarios from above.
Scenario: Find added book
Given An empty bookstore
When Add "a book" by "somebody" with id 123
Then Bookstore has "a book" by "somebody"
// create a regular statement, in this case there is nothing to initialize with an empty bookstore
GIVEN("^An empty bookstore$")
{
// nothing to do for an empty bookstore
}
// we use regular regex statements to extract your placeholders
WHEN("^Add \"(.*?)\" by \"(.*?)\" with id (\\d+)$")
{
// and youse cucumbers REGEX_PARAM(..) macro to get the values into variables
REGEX_PARAM(std::string, title);
REGEX_PARAM(std::string, author);
REGEX_PARAM(std::size_t, id);
// then we make the according calls into our code
cwt::book b{author, title};
details::get_bookstore().add_book(id, b);
}
// and finally we use EXPECT_TRUE(..) from googletest to validate the test case
THEN("^Bookstore has \"(.*?)\" by \"(.*?)\"$")
{
REGEX_PARAM(std::string, title);
REGEX_PARAM(std::string, author);
cwt::book book{author, title};
EXPECT_TRUE(details::get_bookstore().has(book));
}
And thats basically it, build and the first test is ready to run. To access all the variables in the statements we use regular expressions. There are different ways to get to it. I usually use:
(\\d+)
for numbers(\\w+)
for single words without quotes\"(.*?)\"
for a string in quotes
I implemented the following steps in this example (./src/cucumber/step_definitions.cpp
):
GIVEN("^A bookstore with following books$")
GIVEN("^An empty bookstore$")
WHEN("^Add \"(.*?)\" by \"(.*?)\" with id (\\d+)$")
WHEN("^Remove book with id (\\d+)$")
WHEN("^Remove all books$")
THEN("^Bookstore has \"(.*?)\" by \"(.*?)\"$")
THEN("^Total books count is (\\d+)$")
Where then some test can look like (you can find all tests in ./src/cucumber/features/example.feature
):
Scenario: Find added book
Given An empty bookstore
When Add "a book" by "somebody" with id 123
Then Bookstore has "a book" by "somebody"
Scenario: Add books to an empty bookstore
Given An empty bookstore
When Add "a book" by "somebody" with id 123
And Add "another book" by "another author" with id 456
Then Total books count is 2
In general all the macros GIVEN
, WHEN
and THEN
expand to CUKE_
, so technically there is no difference between them, as I understood. They improve the readability of your source code and in your feature file you can use:
- Given
- When
- Then
- And
- But
Providing Longer Strings
In case you have longer strings, you can use """
in your feature file. In cucumber they are called doc strings. I added an example where you can setup a bookstore with a json file. Check out the test case:
Scenario: Initialize a bookstore with a json file
Given A bookstore with following books
"""
{
"books": [
{
"id": "123456",
"author": "Buzz Michelangelo",
"title": "The Story Of Buzz Michelangelo"
},
{
"id": "456789",
"author": "Moxie Crimefighter",
"title": "How To Fight Crimes"
}
]
}
"""
Then Total books count is 2
But Add "some other book" by "Jeff" with id 999
Then Total books count is 3
Which you can simply extract by using this (I guess cucumber extracts any value which is trailing if there wasn't any specified, but I honeslty don't know exactly).
GIVEN("^A bookstore with following books$")
{
REGEX_PARAM(std::string, books);
// the boost json reader expects a string stream here ...
std::stringstream ss;
ss << books;
details::get_bookstore().add_books_from_json(ss);
}
Scenario Outline
Scenario Outline
are pretty helpful if you want to run a test case multiple times with different values. Cucumber allows you to insert placeholders inside angle brackets <>
and define your values in a table (Examples
) then. Consider the following test case, which will run four times:
Scenario Outline: Adding multiple books with scenario outline
Given An empty bookstore
When Add "<book>" by "<author>" with id <id>
Then Total books count is 1
And Bookstore has "<book>" by "<author>"
Examples:
| book | author | id |
| example book 1 | author 1 | 1 |
| example book 2 | author 2 | 2 |
| example book 3 | author 3 | 3 |
| example book 4 | author 4 | 4 |
And Much More
Similar to setup and teardown methods from other testframeworks you can implement in your step definition following functions (see ./src/cucumber/step_definitions.cpp
):
BEFORE()
called before every scenarioAFTER()
called after every scenarioBEFORE_ALL()
called once before all scenariosAFTER_ALL()
called once after all scenarios
You can add tags to scenarios to trigger functions in your test executable. Tags are added with @tag_name
before the dedicated Scenario. Implement following functions, you can concatenate the tags with a ","
:
BEFORE("@foo")
BEFORE("@foo,@bar,@baz")
AFTER("@foo")
And most likely there is more functionality which I haven't used yet.
Conclusion
Find this example here on GitHub, build it and play around with it. Get familiar with this framework and integrate it into your projects, because it is worth it. You have a way better foundation to discuss funcitonality of your code to non software developoers, because they can read and understand it.
And this also includes other positiv sideeffects, like higher code coverage / more tests, writing tests is way easier for your tester and it's sort of a documentation of your code.
Find the documentation of cucumber on their website if you're interested or leave a comment below.
That's it for now.
Best Thomas