Erste Schritte mit cucumber-cpp
By Urs Fässler
Cucumber-cpp bietet die Infrastruktur um Behaviour Driven Development (BDD) mit C++ oder C zu machen. In diesem Beitrag zeige ich wie du deinen Akzeptanztest mit cucumber-cpp implementierst.
Umsetzen wollen wir ein Lauflicht. Einerseits ist es eine genug kleine Aufgabe, anderseits sind Lauflichter einfach cool. So ein Lauflicht hat zwar nicht wirklich viel Verhalten (Behaviour), aber es geht hauptsächlich um die Infrastruktur. Natürlich kann man so ein Lauflicht auch noch beliebig kompliziert machen.
Ich gehe nach der Idee von Test Driven Development (TDD) vor. Zuerst definiere ich das Ziel und wir sehen was notwendig ist um es zu erreichen. Wenn du mitmachen willst, mach ein Terminal auf und erstelle ein Projektverzeichnis in das du wechselst.
Spezifikation ausführen
Das erste Ziel ist die Spezifikation auszuführen. Das machen wir mit cucumber:
$ cucumber
bash: cucumber: command not found
Cucumber ist nicht vorhanden. Wir installieren die Abhängigkeiten und Cucumber, dann probieren wir es erneut:
$ sudo apt install ruby-dev gcc libffi-dev make
$ sudo gem install cucumber -v 7.1.0
$ cucumber
No such file or directory - features. You can use `cucumber --init` to get started.
Cucumber funktioniert, aber der Dateistruktur nach ist es kein Cucumber-Projekt. Folgen wir dem Vorschlag und probieren es erneut:
$ cucumber --init
create features
create features/step_definitions
create features/support
create features/support/env.rb
$ cucumber
0 scenarios
0 steps
0m0.000s
Das sieht gut aus. Cucumber läuft erfolgreich durch, es gibt einfach noch keine Akzeptanz-Tests. Das lässt sich beheben indem wir die Datei features/chase-lighting.feature erstellen:
# language: en
Feature: chase lighting indicator
As an embedded developer
I want to indicate the state of the device with chase lighting
In order to make a pleasant surprise for the user
Scenario: simple chase lighting
When the system is booted
Then the lights are "1000"
Was passiert nun?:
$ cucumber
# language: en
Feature: chase lighting indicator
As an embedded developer
I want to indicate the state of the device with chase lighting
In order to make a pleasant surprise for the user
Scenario: simple chase lighting
When the system is booted
Then the lights are "1000"
1 scenario (1 undefined)
2 steps (2 undefined)
0m0.007s
You can implement step definitions for undefined steps with these snippets:
When('the system is booted') do
pending # Write code here that turns the phrase above into concrete actions
end
Then('the lights are {string}') do |string|
pending # Write code here that turns the phrase above into concrete actions
end
Die Spezifikation wird gefunden und Cucumber schlägt uns vor wie wir es implementieren können. Aber warte, ist das C++? Nein, das ist Ruby, denn Cucumber ist in dieser Sprache geschrieben.
Damit das ganze mit C++ funktioniert brauchen wir zusätzlich cucumber-cpp. Dies startet ein Server mit welchem sich Cucumber dann verbindet.
Mit Cucumber-cpp ausführen
Um cucumber-cpp benutzen zu können müssen wir es herunterladen, kompilieren und installieren. Dies machen wir in einem temporären Verzeichnis:
$ sudo apt install git g++ cmake libasio-dev libtclap-dev nlohmann-json3-dev libgtest-dev
$ git clone https://github.com/cucumber/cucumber-cpp.git
$ cd cucumber-cpp/
$ mkdir build && cd build
$ cmake ../ -DCUKE_ENABLE_GTEST=ON
$ cmake --build . --parallel
$ sudo cmake --install .
Wie wird das nun benutzt? Dazu müssen wir ein Projekt erstellen und gegen cucumber-cpp linken. Dazu erstellen wir im Projektverzeichnis die Datei CMakeLists.txt:
cmake_minimum_required(VERSION 3.15)
add_executable(feature-tests
features/step_definitions/steps.cpp
)
set_target_properties(feature-tests PROPERTIES
CXX_STANDARD 20
)
target_link_libraries(feature-tests PRIVATE
cucumber-cpp
gtest
)
Erstelle auch die leere Datei features/step_definitions/steps.cpp. CMake will eine Datei zum kompilieren haben und wir werden sie später benötigen. Kompiliere das Projekt nun im Verzeichnis build um das Programm build/feature-tests zu erstellen. Da dies der Server ist muss es vor Cucumber im Hintergrund gestartet werden:
$ ./build/feature-tests &
$ cucumber
1 scenario (1 undefined)
2 steps (2 undefined)
0m0.007s
You can implement step definitions for undefined steps with these snippets:
When('the system is booted') do
pending # Write code here that turns the phrase above into concrete actions
end
Then('the lights are {string}') do |string|
pending # Write code here that turns the phrase above into concrete actions
end
Sieht noch gleich wie vorher aus. Cucumber weiss noch gar nicht, dass es sich mit cucumber-cpp verbinden soll. Und das Programm build/feature-tests wartet jetzt noch im Hintergrund auf eine Verbindung. Das muss jetzt manuell beendet werden.
Mit der Konfiguration features/step_definitions/cucumber.wire sagen wir cucumber, dass es sich mit einem build/feature-tests verbinden soll:
host: localhost
port: 3902
Starten wir das ganze nochmals:
$ ./build/feature-tests &
$ cucumber
1 scenario (1 undefined)
2 steps (2 undefined)
0m0.011s
You can implement step definitions for undefined steps with these snippets:
When('the system is booted') do
pending # Write code here that turns the phrase above into concrete actions
end
WHEN("^the system is booted$") {
pending();
}
Then('the lights are {string}') do |string|
pending # Write code here that turns the phrase above into concrete actions
end
THEN("^the lights are \"1000\"$") {
pending();
}
Jetzt sehen wir die Step-Snippets in Ruby und C++. Das bedeutet, dass sich cucumber erfolgreich mit unserer feature-tests Applikation verbunden hat welche mit cucumber-cpp den Wire-Server am laufen hat. Cucumber-cpp läuft!
Steps und Produktivcode
Wir übernehmen die Snippets von cucumber-cpp nach features/step_definitions/steps.cpp:
#include <gtest/gtest.h>
#include <cucumber-cpp/autodetect.hpp>
WHEN("^the system is booted$") {
pending();
}
THEN("^the lights are \"1000\"$") {
pending();
}
Wieder alles kompilieren und ausführen:
$ ./build/feature-tests &
$ cucumber
Scenario: simple chase lighting
When the system is booted
Then the lights are "1000"
1 scenario (1 pending)
2 steps (1 skipped, 1 pending)
0m0.010s
Wir sehen, dass die Steps gefunden werden. Machen wir uns an die Implementierung in der Datei core/ChaseLighting.h:
#pragma once
template<typename T>
class ChaseLighting {
public:
ChaseLighting(T& lights) :
lights{lights} {
}
void boot() {
}
void tick() {
}
private:
T& lights;
};
Den Produktivcode müssen wir in features/step_definitions/steps.cpp includieren und mit den Steps verbinden:
#include <gtest/gtest.h>
#include <cucumber-cpp/autodetect.hpp>
#include "core/ChaseLighting.h"
struct Context {
std::array<bool, 4> lights{};
ChaseLighting<std::array<bool, 4>> chaseLighting{lights};
};
template<typename T>
std::string toString(const T& lights) {
std::string result(lights.size(), '0');
std::transform(lights.begin(), lights.end(), result.begin(), [](bool b) {
return b ? '1' : '0';
});
return result;
}
WHEN("^the system is booted$") {
cucumber::ScenarioScope<Context> context{};
context->chaseLighting.boot();
}
THEN("^the lights are \"1000\"$") {
cucumber::ScenarioScope<Context> context{};
const auto actual = toString(context->lights);
ASSERT_EQ("1000", actual);
}
Mache die notwendigen Anpassungen an CMakeLists.txt und kompiliere das Projekt. Dann führen wir erneut alles aus:
$ ./build/feature-tests &
$ cucumber
Scenario: simple chase lighting
When the system is booted
features/step_definitions/steps.cpp:28: Failure
Expected equality of these values:
"1000"
actual
Which is: "0000"
Then the lights are "1000"
features/step_definitions/steps.cpp:28: Failure
Expected equality of these values:
"1000"
actual
Which is: "0000" (Cucumber::Wire::Exception)
features/chase-lighting.feature:10:in `the lights are "1000"'
Failing Scenarios:
cucumber features/chase-lighting.feature:8
1 scenario (1 failed)
2 steps (1 failed, 1 passed)
0m0.013s
Das sieht gut aus. Unsere Steps benutzen den Produktivcode, welcher noch nicht das richtige macht. Wir implementieren diesen, kompilieren das Projekt und führen es erneut aus:
$ ./build/feature-tests &
$ cucumber
Scenario: simple chase lighting
When the system is booted
Then the lights are "1000"
1 scenario (1 passed)
2 steps (2 passed)
0m0.010s
Sehr cool, der erste Akzeptanztest mit Cucumber und C++ ist erfolgreich durchgelaufen! Die ersten Schritte sind gegangen, äehm, Steps implementiert 😉. Erweitere die Spezifikation für ein vollständiges Lauflicht. Und wenn es langweilig wird kannst du ja eine Möglichkeit schaffen um das Muster zu ändern.
Applikation Integration
Den Code innerhalb des Akzeptanztests laufen zu lassen ist ja super um zu verifizieren dass wir diese erfüllen, aber schlussendlich wollen wir eine Applikation. Probieren wir das mal aus:
$ ./build/chase-lighting
bash: ./build/chase-lighting: No such file or directory
Die gibt es noch nicht. Also erstellen wir sie unter app/main.cpp:
int main() {
return 0;
}
Nach dem hinzufügen im CMake und kompilieren können wir es wieder probieren:
$ ./build/chase-lighting
Die Applikation wird ausgeführt, aber kein Lauflicht. Damit das klappt müssen wir den bereits erstellten Produktivcode von core/ChaseLighting.h in app/main.cpp verwenden und mit etwas Glue-Code kompletieren:
template<typename T>
void displayLights(const T& lights) {
std::cout << '\r';
for (const auto light : lights) {
const auto sym = light ? "🟢" : "⚫";
std::cout << sym;
}
std::cout << " " << std::flush;
}
int main() {
std::array<bool, 8> lights{};
ChaseLighting chaseLighting{lights};
chaseLighting.boot();
displayLights(lights);
while (true) {
std::this_thread::sleep_for(std::chrono::milliseconds(200));
chaseLighting.tick();
displayLights(lights);
}
return 0;
}
Alles wieder kompilieren und nochmals ausprobieren:
Fazit
Im Artikel habe ich gezeigt, wie Du cucumber-cpp bekommst und aufsetzt. Die integration im Projekt für Test und Produktivcode haben wir ebenfalls angeschaut. Damit ist der Weg frei um weitere Schritte in diese Richtung zu gehen.