A minimalistic, header-only BDD framework for modern C++.
Write behavior-driven tests as plain C++ functions and compose them with a fluent Given / With / When / Then API. No dependencies, no code generation — the test is the specification, and the readable output comes for free from your function names. Gherkin support (runtime .feature file interpreter) is included by default, but you are never required to use it: plain C++ functions are still the primary, zero-ceremony way to write specs. For pure C++17 builds or consumers who explicitly don't want Gherkin, define BABYBEHAVE_DISABLE_GHERKIN before including the header.
Given a: FreshlyBootedCoffeeMachine
With: AFullWaterTank
When: IBrewAnEspresso
Then: ACupIsServed
And: TheTankLevelDecreases
- Header-only: drop
include/BabyBehave/bdd.hppinto your project and you're done - C++23, with graceful C++17 fallback: the header uses
<version>feature-test macros to pick the best available standard library facility at each call site (see C++ standard support below) — no hard C++23 requirement on the header itself - Zero dependencies: standard library only
- Fluent BDD vocabulary:
Given,With,When,Then,And,Or,But(plusGivenA,WithI,WhenI,ThenI, … variants for readable English), with an opt-out for consumers whose codebase already uses those names (see Customizing the macros) - Shared test context: a
std::any-backed key/value store passes state between steps, with an optional compile-time-checked key type for consumers who want to avoid stringly-typed lookups (see TestContext) - Customizable failure handling: plug in your own callbacks for failed conditions and exceptions
- Exception-safe by construction: failures inside steps, context setup, or your own callbacks are all caught and routed safely — nothing escapes into the (
noexcept) destructor and triggersstd::terminate() - Result objects when you want them: opt into
SetCollectFailuresMode(true)and every step's outcome is collected into aTestResultinstead of stopping at the first failure (see Collecting results instead of exiting) - Multi-assertion steps:
SoftChecklets one step record several named sub-checks that aggregate into a single readable failure message (see Soft checks: multiple assertions per step) - Call-site diagnostics: with
std::source_locationsupport, every step's call site is captured automatically and surfaced in failure messages andStepResult::location, with zero source changes required (see Call-site diagnostics) - Fluent matchers (optional, standalone
matchers.hpp):Expect(value).ToEqual(...),.ToBeTrue(), and friends for descriptive actual-vs-expected failure messages (see Fluent matchers) - Structured CI output (optional
reporters.hpp): serialize collected results to JUnit XML or TAP for your CI dashboard of choice (see Structured output: JUnit XML / TAP) - CMake install/export support: consume it via
FetchContent/add_subdirectoryorfind_package(BabyBehave REQUIRED)after installing it (see Installation) - MIT licensed
A step is any function taking a TestContext& and returning bool. The context setup function (used by Given) returns void.
#include <BabyBehave/bdd.hpp>
using namespace BabyBehave::BDD;
void FreshlyBootedCoffeeMachine(TestContext& ctx) {
ctx.Set("machine", std::make_shared<CoffeeMachine>());
}
bool AFullWaterTank(TestContext& ctx) {
ctx.Get<std::shared_ptr<CoffeeMachine>>("machine")->FillTank();
return true;
}
bool IBrewAnEspresso(TestContext& ctx) {
return ctx.Get<std::shared_ptr<CoffeeMachine>>("machine")->Brew();
}
bool ACupIsServed(TestContext& ctx) {
return ctx.Get<std::shared_ptr<CoffeeMachine>>("machine")->CupsServed() == 1;
}
int main() {
Given(FreshlyBootedCoffeeMachine)
.With(AFullWaterTank)
.When(IBrewAnEspresso)
.Then(ACupIsServed);
}The macros stringify the function names, so the scenario above prints itself as the spec shown at the top — naming your steps well is writing your documentation.
Given(fn)creates aBabyBehaveTestand runsfnto set up theTestContext.- Each chained step (
With,When,Then,And,Or,But) registers a typed step (Precondition,Action,Postcondition,And,Or,But) viaAddStep<StepType>(name, fn). - The scenario executes when the test object goes out of scope; steps run in order and each one's return value is verified.
- Step types are stored in a
std::variantand dispatched withstd::visit+if constexpr, so each keyword gets its own labelled output ("With:", "When:", "Then:", …) and failure message.
TestContext is a std::any-based store shared across all steps of a scenario. There are two ways to use it:
Plain string keys — quick to write, but a typo'd key or a mismatched type only fails at runtime (std::out_of_range if the key is missing, std::bad_any_cast if the type doesn't match):
ctx.Set("answer", 42);
int x = ctx.Get<int>("answer");Typed ContextKey<T> — an opt-in, compile-time-checked key you declare once per logical value. It's purely additive sugar over the same underlying string-keyed storage, so it interoperates freely with the plain API above:
static constexpr BabyBehave::BDD::TestContext::ContextKey<int> kAnswerKey{"answer"};
ctx.Set(kAnswerKey, 42);
int x = ctx.Get(kAnswerKey); // wrong-type Set/Get calls fail to compile instead of throwingIn-place mutation and lazy initialization — for scenarios that need to modify shared state directly (without copy-mutate-writeback ceremony) or lazily initialize values:
// Mutate<T>(key) returns a live reference to mutate the stored value in place
std::vector<int>& items = ctx.Mutate<std::vector<int>>("items");
items.push_back(42);
// GetOrInit<T>(key, init) inserts init only if key is absent, returns reference
std::shared_ptr<Connection>& conn = ctx.GetOrInit<std::shared_ptr<Connection>>("db",
std::make_shared<Connection>("localhost"));
conn->Query("SELECT ..."); // conn already initialized if this isn't the first stepBoth come in string-keyed and Key<T>-keyed variants for consistency.
The fluent keywords (Given, With, When, Then, And, Or, But, and their ...I variants) are implemented as macros so they can stringify your function names. They use capitalized identifiers to stay clear of the C++ alternative tokens and/or, but a name like And or When can still collide with another library or with your own code.
If that happens, define BABYBEHAVE_NO_SHORT_MACROS before including the header to skip defining the short macros entirely, and call the underlying API directly instead: GivenAImpl("name", fn) to start a scenario and AddStep<StepType>("name", fn) (with StepType one of Precondition, Action, Postcondition, And, Or, But) to add each step. See examples/NoShortMacros.cpp for a complete, working example of this style:
#define BABYBEHAVE_NO_SHORT_MACROS
#include <BabyBehave/bdd.hpp>
using namespace BabyBehave::BDD;
int main() {
GivenAImpl("an empty basket", EmptyBasket)
.AddStep<Precondition>("BasketIsEmpty", BasketIsEmpty)
.AddStep<Action>("AddItemToBasket", AddItemToBasket)
.AddStep<Postcondition>("BasketHasOneItem", BasketHasOneItem);
}By default a failed condition or an uncaught exception prints an error and exits with EXIT_FAILURE. Both behaviors are injectable, which makes BabyBehave easy to embed in another test runner:
auto test = Given(FreshlyBootedCoffeeMachine);
test.SetOnConditionNotVerifiedCallback([](const std::string& msg) {
/* report to your framework instead of exiting */
});
test.SetOnExceptionCallback([](const std::string& step, const std::exception& e) {
/* ... */
});Exceptions are contained at every boundary: a throw from context setup, from any step, or even from your own SetOnConditionNotVerifiedCallback/SetOnExceptionCallback is caught and never allowed to propagate out of BabyBehaveTest's destructor (which is where scenario execution actually happens). Non-std::exception throws (e.g. throw 42;) are caught too and reported with a generic message instead of crashing the process. tests/bdd/test_SelfTest.cpp exercises every one of these paths directly — see Self-hosted dogfood example below.
On toolchains with <source_location> support (guarded by __cpp_lib_source_location, the same feature-test-macro pattern as the C++23 facilities in C++ standard support below), every AddStep<StepType>(...) call — including the ones the With/When/Then/And/Or/But macros expand to, and the Given/GivenA context-setup call — automatically captures its own call site via a std::source_location::current() default parameter, with zero source changes needed on your part. The formatted "file:line" shows up in two places:
- Appended to the failure message passed to
SetOnConditionNotVerifiedCallback/SetOnExceptionCallback(and to the defaultstd::exit-ing callbacks) as" (at file:line)", pointing straight at the failingWith(...)/When(...)/Then(...)/... line. - In
StepResult::location, for every step (pass or fail), when running underSetCollectFailuresMode(true)(see Collecting results instead of exiting);reporters.hpp'sToJUnitXml()uses it to populate<testcase file="..." line="...">.
On toolchains without <source_location>, location is always the empty string and no message suffix is appended — everything else behaves identically.
tests/bdd/test_SelfTest.cpp is BabyBehave testing BabyBehave, using its own Given/With/When/Then API to drive scenarios and check that the library behaves the way its contract promises (happy path, failed preconditions/actions, thrown std::exceptions, thrown non-std::exception values, context-setup exceptions, TestContext round-tripping, collect-failures mode, and SoftCheck).
Because the default failure callbacks call std::exit(), and BabyBehaveTest::Execute() runs from the destructor, test_SelfTest.cpp installs its own SetOnConditionNotVerifiedCallback/SetOnExceptionCallback on every scenario to record the outcome instead of exiting, then inspects what was recorded once each scenario's BabyBehaveTest goes out of scope. This is the same pattern you'd use to embed BabyBehave scenarios inside your own test harness (gtest, Catch2, a CI script, …) instead of letting a failure kill the whole process.
By default, a failed step invokes SetOnConditionNotVerifiedCallback/SetOnExceptionCallback (which default to printing to std::cerr and calling std::exit(EXIT_FAILURE)), and execution stops at the first failure. BabyBehaveTest::SetCollectFailuresMode(true) switches to a different mode: every step still runs in order, its outcome is appended to an internal TestResult, and execution continues with the rest of the chain instead of stopping or invoking the failure callbacks at all.
struct StepResult {
std::string stepLabel; // "Precondition", "Action", "Postcondition", "And", "Or", "But", or "ContextSetup"
std::string stepName; // the function name captured by With/When/Then/...
bool passed = true;
std::string message; // empty when passed; the failure/exception message otherwise
std::string location; // "file:line" of the call site (see Call-site diagnostics above)
};
struct TestResult {
std::string testName;
bool allPassed = true; // AND of every recorded StepResult::passed
std::vector<StepResult> steps;
};Execute() is what actually runs the scenario (context setup, then every step) — it's what the destructor calls implicitly, but it's also public and idempotent: the first call runs the scenario and caches the TestResult, and any later call (including the implicit one from the destructor) just returns the cached result. Because the destructor is what normally triggers execution, and the BabyBehaveTest object is gone by the time its destructor returns, a consumer who wants the TestResult can't rely on the usual fire-and-forget macro chain — it has to bind the test to a named variable and call Execute() explicitly while the object is still alive:
BabyBehaveTest test = GivenA(SetupTrivialContext);
test.SetCollectFailuresMode(true);
test.With(StepPreconditionFalseForCollect)
.When(StepActionTrueForCollect)
.And(StepAndThrowsForCollect)
.Then(StepThenTrueForCollect);
const TestResult& result = test.Execute();
// result.allPassed is false; result.steps has one StepResult per step, in
// order, including the ones that passed (test's destructor is now a no-op).(adapted from RunCollectFailuresModeScenario in tests/bdd/test_SelfTest.cpp; GetResult() returns the same TestResult without re-running anything, for inspecting it again later.)
Off by default, so a consumer who never calls SetCollectFailuresMode(true) sees byte-identical behavior to before this feature existed.
A step is still a single bool(TestContext&), but sometimes you want to check several independent things in one step and see all of them in the failure message, not just a generic "Action failed". SoftCheck is an opt-in recorder for that:
bool StepActionWithSoftChecks(TestContext& context) {
SoftCheck checks(context);
const int count = 15;
checks.Check("has valid id", true);
checks.Check("name matches", true);
checks.Check("count in range", count >= 1 && count <= 10, "count was " + std::to_string(count));
return checks.AllPassed();
}Check(label, condition, message = "") records one named sub-check and returns condition unchanged (handy for early-return patterns); AllPassed() is the AND of every Check() call made so far (true if Check() was never called, so a step that only conditionally checks is unaffected). If the step's overall bool comes back false and at least one sub-check failed, the failure message is extended with the failing sub-checks — for the snippet above, that's "Action failed: count in range (count was 15)". Passing sub-checks are omitted from the message; only the failing ones are useful. This applies both to the default failure-callback path and to SetCollectFailuresMode(true)'s StepResult::message — see tests/bdd/test_SelfTest.cpp's RunSoftCheckCollectFailuresScenario/RunSoftCheckDefaultCallbackScenario for both. A step that never constructs a SoftCheck is completely unaffected — this is purely additive.
condition is a plain, eagerly-evaluated bool, so SoftCheck::Check composes with raw comparisons or with BabyBehave::Matchers::Expect(...).ToXxx(...) (see Fluent matchers below) exactly as well as with anything else — SoftCheck only adds naming/grouping on top.
include/BabyBehave/matchers.hpp is a small, standalone, dependency-free helper for more descriptive failure messages inside a step body. It has no include on bdd.hpp and no knowledge of TestContext/BabyBehaveTest, so it's just as usable outside BabyBehave entirely — it's a separate header (like reporters.hpp below) so consumers who don't want it never pay for it.
#include <BabyBehave/matchers.hpp>
using namespace BabyBehave::Matchers;
bool AlarmWillRing(TestContext& context) {
auto alarmClock = context.Get<std::shared_ptr<AlarmClock>>("AlarmClock");
return Expect(alarmClock->GetHour()).ToEqual(7);
}Expect(value) returns an Expectation<T> with:
ToEqual(expected)/ToNotEqual(expected)ToBeTrue()/ToBeFalse()ToBeGreaterThan(expected)/ToBeGreaterOrEqualTo(expected)ToBeLessThan(expected)/ToBeLessOrEqualTo(expected)ToContain(item)— substring search for string-like types (std::string,std::string_view,const char*), element search (viastd::find) for anything usable withstd::begin/std::endToBeNull()/ToNotBeNull()— works on raw/smart pointers and anything nullptr-comparable, and on optional-like types (anything with a.has_value()member)
Every ToXxx() returns bool, so it can be used directly as a step's return ...;; on failure it prints a descriptive "Expect(...) failed: expected <actual> to <verb> <expected>" message to std::cerr before returning false (values that aren't stream-insertable print as "(non-printable value)" instead of failing to compile). See examples/Matchers.cpp for a complete worked example.
include/BabyBehave/reporters.hpp turns a collected TestResult/StepResult (see Collecting results instead of exiting) into two CI-friendly formats:
#include <BabyBehave/reporters.hpp>
std::vector<TestResult> results = { result1, result2, result3 };
std::cout << BabyBehave::BDD::Reporters::ToJUnitXml(results, "BabyBehave.SelfTest") << '\n';
std::cout << BabyBehave::BDD::Reporters::ToTap(results) << '\n';ToJUnitXml(results, suiteName = "BabyBehave")— a single<testsuite>with one<testcase classname="{testName}" name="{stepLabel}: {stepName}">perStepResult; a failed one gets a nested<failure message="...">, and a captured source location is split intofile/line<testcase>attributes. Understood by GitHub Actions test-report actions, GitLab's JUnit report artifact type, Jenkins' JUnit plugin, and most other CI dashboards.ToTap(results)— Test Anything Protocol output: a1..Nplan line followed by oneok/not okline perStepResult, with a# messagediagnostic line under failing ones. Consumable byproveand other generic TAP harnesses.
Both also have single-TestResult convenience overloads, and both are pure: they format and return a std::string, with no file I/O of their own — it's up to the caller to print it or write it wherever CI expects it.
Like matchers.hpp, this lives in its own header rather than in bdd.hpp (it #includes "bdd.hpp" itself, since it exists specifically to format TestResult/StepResult), so consumers who don't want it don't pay for it. It only makes sense for scenarios run under SetCollectFailuresMode(true): in the default mode a failed step invokes the (by default std::exit-ing) failure callbacks before Execute() ever returns, so there is no complete TestResult to serialize in that case. Only feed it TestResults from SetCollectFailuresMode(true) scenarios (a scenario that passed entirely still produces a valid, empty-but-meaningful TestResult). See tests/bdd/test_SelfTest.cpp, which accumulates results from its collect-failures-mode scenarios and writes both selftest-results.xml and selftest-results.tap at the end of main().
BabyBehave includes a runtime .feature file interpreter for teams that prefer Gherkin's structured syntax alongside BabyBehave's fluent C++ DSL. It is on by default but can be disabled via BABYBEHAVE_DISABLE_GHERKIN (see below for when you might want to).
#include <BabyBehave/bdd.hpp>
using namespace BabyBehave::BDD;
using namespace BabyBehave::BDD::Gherkin;
int main() {
StepRegistry registry;
// Register step definitions: pattern + keyword(s) + implementation.
registry.RegisterGiven("an empty basket", [](TestContext& ctx) {
ctx.Set("basket", std::make_shared<Basket>());
return true;
});
registry.RegisterStep({Keyword::When, Keyword::And}, "I add {int} apples", [](TestContext& ctx, int count) {
ctx.Get<std::shared_ptr<Basket>>("basket")->Add("apple", count);
return true;
});
registry.RegisterThen("the basket contains {int} items", [](TestContext& ctx, int expected) {
return ctx.Get<std::shared_ptr<Basket>>("basket")->Count() == expected;
});
const auto feature = R"feature(
Feature: Shopping basket
Scenario: Adding items to a basket
Given an empty basket
When I add 3 apples
And I add 2 oranges
Then the basket contains 5 items
)feature";
const auto result = Feature(feature, registry).Label("Shopping basket").Run();
return result.ExitCode();
}Step registration ergonomics: RegisterStep(keywords, pattern, fn) registers a step for multiple keywords at once (e.g. {Keyword::When, Keyword::And} registers the same pattern for both When and And steps). For bulk declarative registration of many steps, RegisterSteps(StepEntry<F1>{...}, StepEntry<F2>{...}, ...) accepts a variadic list of StepEntry<F> structures (each with a keyword, pattern, and function), enabling a clean table-like registration style for domains with 10+ steps. Both are purely additive — RegisterGiven/RegisterWhen/RegisterThen/RegisterAnd/RegisterBut and the positional RunFeature() API remain fully supported.
Feature execution builder: The Feature(text, registry) and FeatureFromFile(path, registry) factory functions return a FeatureRun builder that offers .Label(name), .OnFailure(callback), .Parallel(bool), and .Run() methods for named-parameter ergonomics. Run() returns a FeatureResult with an ExitCode() method for portable exit-code decision-making, complementing the pre-existing positional RunFeature() signature. LoadFeatureFile(path) is a standalone utility for reading .feature files from disk.
Error reporting: Structural .feature file parse errors are now collected and reported in one pass (one onFailure call per error, format "<file>:<line>: parse error: <message>") instead of stopping at the first. Scenario failure messages are a single concise line summarizing the outcome, with full per-step detail still available via FeatureResult::scenarioResults[i].steps for programmatic inspection.
The interpreter supports:
- Feature labels and Scenarios — organized test flows with readable names
- Background steps — shared preconditions for multiple scenarios
- Step parameters —
{int},{float},{string},{word}placeholders with automatic type conversion - Tags —
@tagannotations for scenario filtering and hook registration - Before/After hooks — tag-scoped setup/teardown via
AddBeforeHook()/AddAfterHook(), or combined viaAddAroundHook()for Before+After pairs - Suite-level Before/After-all hooks — one-time setup/teardown across all Scenarios in a Feature via
AddBeforeAllHook()/AddAfterAllHook(); Before-ALL runs once before any Scenario, After-ALL runs once after all Scenarios (guaranteed only with a custom non-exitingonFailurecallback) - Tag expressions (AND/OR/NOT) — boolean tag-matching expressions for conditional hooks via
AddBeforeHookExpr()/AddAfterHookExpr()/AddAroundHookExpr(); supportsand/or/notkeywords with parentheses for grouping - Comments —
# commentsin.featurefiles are parsed and ignored - Timeout annotations —
@timeout:<value><unit>tags for scenario-level deadline checking (cooperative inter-step only, no preemptive interruption) - Scenario Outline / Examples — data-driven scenario expansion with
<placeholder>tokens in step text and pipe-delimitedExamples:tables - Data Tables — tabular arguments to steps with header-aware cell lookups, opt-in via trailing
const DataTable¶meter - Doc Strings — multi-line string arguments to steps (triple-quote-delimited blocks), passed as
const std::string¶meters with smart indentation stripping - Parallel scenario execution — concurrent scenario runs within a Feature via
RunFeature(..., onFailure, enableParallelScenarios=true)with a custom non-exiting failure callback - Retry/flaky annotations —
@retry:Ntags for automatic re-attempts on scenario failure (N total attempts, not extra retries), stopping at the first success; every attempt is a full, independent re-run of Before hooks/Background/Steps/After hooks
RunFeature() also takes an optional fourth onFailure parameter (Gherkin::GherkinFailureCallback, i.e. std::function<void(std::string_view)>) for redirecting Gherkin-sourced failures (a parse error or a failing Scenario) to your own handler instead of the library's default print-and-exit(EXIT_FAILURE) behavior:
FeatureResult RunFeature(std::string_view featureText, StepRegistry& registry,
std::string_view featureLabel = "<feature>",
const GherkinFailureCallback& onFailure = impl::DefaultGherkinFailureAction,
bool enableParallelScenarios = false);Alternatively, use the builder API for named-parameter style: Feature(text, registry).Label(...).OnFailure(...).Parallel(...).Run() or FeatureFromFile(path, registry).OnFailure(...).Run() (the latter defaults its label to the file path). A callback that returns normally instead of exiting/throwing lets execution continue across the whole Feature and return a FeatureResult with allPassed=false for you to inspect — see GherkinCustomFailureHandler.cpp below. Gherkin::CollectingFailureHandler is a provided implementation that appends each message to a std::vector<std::string> instead of exiting, useful for concurrent or multi-scenario test runs.
Three core Gherkin examples live directly in examples/; the rest (including two multi-file registry-reuse demos) live in examples/gherkin/:
GherkinBasket.cpp— demonstratesRegisterStep()bulk registration, theFeature(...).Label(...).Run()builder pattern, andFeatureResult::ExitCode()GherkinBackground.cpp— shared Background steps across multiple scenariosGherkinTagsAndHooks.cpp— tag-scoped@tagfilters and Before/After hook registrationgherkin/GherkinUnmatchedStep.cpp— demonstrating fail-hard behavior on unmatched stepsgherkin/GherkinCollectFailures.cpp— forced collect-failures mode to gather all step outcomesgherkin/GherkinPlaceholders.cpp— all four placeholder types ({int},{float},{string},{word})gherkin/GherkinMultiThreaded.cpp— concurrentRunFeature()calls per thread with independent registriesgherkin/GherkinAdvanced.cpp— combined realistic feature with multiple scenariosgherkin/GherkinVeryAdvanced.cpp— multi-feature scenarios with integration ofreporters.hppgherkin/GherkinCustomFailureHandler.cpp— a customonFailurecallback that collects failure messages instead of exiting
Bakery/Library domain examples — gherkin/BakerySteps.hpp and gherkin/LibrarySteps.hpp are shared step-definition libraries reused via StepRegistry::Merge() across most (not all — a few below build their own standalone registry instead) of the example files in each domain, each with genuinely different scenarios (reading their .feature text from real files under gherkin/features/ via LoadFeatureFile(), not embedded strings). Each example follows a consistent PrepareRegistry() / RunFeatureFromFile() pattern:
gherkin/GherkinBakeryStandardOrder.cpp— standard cake order paid in fullgherkin/GherkinBakeryAllergenSubstitution.cpp— allergen substitution surcharge, plusMerge()for a file-specific stepgherkin/GherkinBakerySeasonalDiscountTiers.cpp— loyalty tier discounts using Scenario Outline with Examples tablegherkin/GherkinBakeryBulkOrderItemized.cpp— bulk order with itemized line items as a Data Table, computing an order totalgherkin/GherkinBakeryLateCancellation.cpp— late cancellation forfeits the deposit (intentionally exits non-zero)gherkin/GherkinBakeryConcurrentOrderProcessing.cpp— concurrent customer order processing withenableParallelScenarios=trueand a custom collectingonFailurecallbackgherkin/GherkinBakeryFlakyOvenSensorRetry.cpp— flaky oven temperature sensor tolerated via@retry:3, deterministically failing its first two read attempts before succeeding on the thirdgherkin/GherkinBakeryDailyOvenLifecycle.cpp— daily oven preheating and cooldown via Before-ALL and After-ALL hooks, with 3 baking scenariosgherkin/GherkinLibraryStandardLending.cpp— checkout and on-time returngherkin/GherkinLibraryHoldsAndReservations.cpp— hold queue fulfillment, plusMerge()for a file-specific stepgherkin/GherkinLibraryOverdueFines.cpp— overdue fine calculationgherkin/GherkinLibraryConcurrentLending.cpp— one sharedStepRegistry, built once, fanned out across four threads each runningRunFeature()against a different branch's.featurefile concurrentlygherkin/GherkinLibraryHoldPickupDeadline.cpp— service-level deadline with@timeout:2sannotation and fast realistic stepsgherkin/GherkinLibraryBookReviewSubmission.cpp— multi-paragraph book review as a Doc String, with word count validation and substring searchgherkin/GherkinLibraryPriorityPatronHandling.cpp— VIP and urgent patron expedited service using tag expressions withAddBeforeHookExpr("@vip or @urgent", ...)
Gherkin support requires C++20 (specifically <concepts> and <optional>), while the rest of BabyBehave gracefully falls back to C++17. If you're targeting pure C++17 or simply don't need Gherkin, define BABYBEHAVE_DISABLE_GHERKIN before including the header:
#define BABYBEHAVE_DISABLE_GHERKIN
#include <BabyBehave/bdd.hpp>
// BabyBehave::BDD::Gherkin is NOT defined; everything else works normally.
// The C++20 includes are not pulled in either, so no compilation penalty.
using namespace BabyBehave::BDD;
// ... write tests using the fluent API as usualFor design decisions, feature coverage rationale, and why only AND/subset tag matching in v0.8.0, see docs/design/gherkin-support.md.
This library recognizes several compile-time #defines and runtime environment variables, including BABYBEHAVE_DISABLE_GHERKIN, BABYBEHAVE_NO_SHORT_MACROS, BABYBEHAVE_QUIET (env var), and BABYBEHAVE_STYLE (env var). For a comprehensive reference with examples, see docs/configuration.md.
Each BabyBehaveTest (created by Given/GivenA) owns its TestContext as a private member — it's never shared between scenarios unless you go out of your way to pass one TestContext& into several of them. That means launching independent scenarios one-per-thread (e.g. via std::async) needs no locking at all: there is nothing for the threads to race on. TestContext itself is not thread-safe (it's backed by a plain std::unordered_map with no internal synchronization), so the one rule is: don't share a single TestContext across threads. See examples/MultiThreaded.cpp for a complete, working example — including an #if 0-guarded, never-compiled sketch of the unsafe shared-TestContext pattern it deliberately avoids.
include(FetchContent)
FetchContent_Declare(
BabyBehave
GIT_REPOSITORY https://github.com/crsnplusplus/BabyBehave.git
GIT_TAG main
)
FetchContent_MakeAvailable(BabyBehave)
add_executable(your_tests your_tests.cpp)
target_link_libraries(your_tests PRIVATE BabyBehave::BabyBehave)The same works with a plain add_subdirectory(BabyBehave) if you vendor the source directly instead of fetching it.
BabyBehave ships a CMake package config, so it can be installed once and consumed from anywhere:
git clone https://github.com/crsnplusplus/BabyBehave.git
cd BabyBehave
cmake -B build -DCMAKE_INSTALL_PREFIX=/your/prefix
cmake --build build
cmake --install buildfind_package(BabyBehave REQUIRED)
add_executable(your_tests your_tests.cpp)
target_link_libraries(your_tests PRIVATE BabyBehave::BabyBehave)Either way, linking against the BabyBehave::BabyBehave target is all you need — it's an INTERFACE target that only adds the include path (and requires C++23, see Requirements below).
BabyBehave isn't in the curated vcpkg registry yet, but the repo ships an overlay port at ports/babybehave/. Until a tagged release exists, install straight from main with --head:
git clone https://github.com/crsnplusplus/BabyBehave.git
vcpkg install babybehave --head --overlay-ports=./BabyBehave/ports/babybehavefind_package(BabyBehave REQUIRED)
target_link_libraries(your_tests PRIVATE BabyBehave::BabyBehave)BabyBehave ships a Conan 2.x recipe (conanfile.py) for a header-only header-library package:
git clone https://github.com/crsnplusplus/BabyBehave.git
cd BabyBehave
conan create . --version 0.7.19Then, from a consumer's conanfile.txt:
[requires]
babybehave/0.7.19
[generators]
CMakeDeps
CMakeToolchainfind_package(BabyBehave REQUIRED)
target_link_libraries(your_tests PRIVATE BabyBehave::BabyBehave)Not yet in the Bazel Central Registry, so depend on the repo directly via git_override (or local_path_override for a vendored copy) in your MODULE.bazel:
bazel_dep(name = "babybehave", version = "0.7.19")
git_override(
module_name = "babybehave",
remote = "https://github.com/crsnplusplus/BabyBehave.git",
commit = "<commit-sha>", # or a v0.7.19 tag once one exists
)# your BUILD.bazel
cc_test(
name = "your_tests",
srcs = ["your_tests.cpp"],
deps = ["@babybehave"],
)Bazel doesn't infer a project-wide C++ standard the way CMake does, so add --cxxopt=-std=c++23 (or -std=c++17, given the fallback story below) to your own .bazelrc.
BabyBehave is genuinely header-only and has zero third-party dependencies, so the simplest possible integration is copying include/BabyBehave/*.hpp straight into your project and adding that directory to your compiler's include path — no build-system integration at all.
Most of bdd.hpp (fluent API, matchers, reporters) is C++17 compatible, with graceful C++23 enhancements where available. The Gherkin block specifically requires C++20 (<concepts>, <optional>, modern regex semantics). Consumers targeting pure C++17 can disable Gherkin via BABYBEHAVE_DISABLE_GHERKIN (see Opting out of Gherkin above).
For the non-Gherkin parts, the header checks the relevant <version> feature-test macros and falls back when a C++23 facility isn't available:
| Facility | C++23 | C++17 fallback | Guarded by |
|---|---|---|---|
| Step/context-setup callable storage | std::move_only_function |
std::function |
__cpp_lib_move_only_function |
| Console output | std::println |
std::cout << ... << '\n' |
__cpp_lib_print |
Step/Given call-site capture (see Call-site diagnostics) |
std::source_location |
unavailable — StepResult::location stays empty, no message suffix |
__cpp_lib_source_location |
So most of the header will still compile under -std=c++17 (except the Gherkin block). Note: the BabyBehave::BabyBehave CMake target declares cxx_std_23 as a compile feature requirement (src/CMakeLists.txt), so consumers who link against that target are bumped to C++23 by CMake regardless. If you need to build under C++17, vendor/copy the header directly instead of linking the CMake target (and optionally define BABYBEHAVE_DISABLE_GHERKIN if you want to be extra conservative).
Two opt-in options are available when configuring the project itself (not needed by consumers just linking BabyBehave::BabyBehave):
# AddressSanitizer + UndefinedBehaviorSanitizer
cmake -B build/bb-debug -DBABYBEHAVE_ENABLE_SANITIZERS=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build/bb-debug
ctest --test-dir build/bb-debug --output-on-failure
# gcov-based code coverage (also builds an HTML `coverage-report` target if lcov + genhtml are found)
cmake -B build/coverage -DBABYBEHAVE_ENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build/coverage
cmake --build build/coverage --target coverage-report # only if lcov/genhtml are installed(Gherkin is now on by default; there is no CMake option to gate it, since disabling it is a consumer's compile-time decision via BABYBEHAVE_DISABLE_GHERKIN.)
With BABYBEHAVE_ENABLE_COVERAGE=ON and gcov available, two independent coverage measurements are also available as build targets:
cmake --build build/coverage --target coverage-ut coverage-bbhcoverage-ut— coverage ofbdd.hppas exercised by the gtest unit test suite intests/.coverage-bbh— coverage ofbdd.hppas exercised by the self-hostedtests/bdd/test_SelfTest.cppdogfood tests.
There are two separate targets, not one, because bdd.hpp is header-only and template-heavy: every binary that includes it compiles and instruments its own private copy of its inline code, with its own .gcno/.gcda pair. Keeping the two measurements' object directories separate keeps the reports naturally isolated from each other.
git clone https://github.com/crsnplusplus/BabyBehave.git
cd BabyBehave
cmake -B build/bb-release
cmake --build build/bb-release
ctest --test-dir build/bb-releaseSee the examples/ and tests/ directories for working scenarios.
- To build this project's own CMake targets (examples, tests, and anything linking
BabyBehave::BabyBehave): a C++23 compiler - To vendor/include
bdd.hppdirectly, outside of this project's CMake:- Default (with Gherkin enabled): a C++20 compiler is required (for
<concepts>and<optional>) - Pure C++17: possible by defining
BABYBEHAVE_DISABLE_GHERKINbefore including (see C++ standard support and Opting out of Gherkin)
- Default (with Gherkin enabled): a C++20 compiler is required (for
- CMake 3.20+ only if you want to build the examples/tests, install the package, or use
find_package; consuming the header directly requires nothing
MIT — © 2023 Cristian Marletta