[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 scenario
  • AFTER() called after every scenario
  • BEFORE_ALL() called once before all scenarios
  • AFTER_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

Previous
Previous

[C++] Building And Publishing Conan Packages

Next
Next

[C++] A C++17 Statemachine using std::tuple and std::variant