I couldn’t find any resources or examples on how to set up Catch2 test framework with cpp project that is built using Bazel, so I thought I would write about it to help other programmers in need! I am new with both Bazel and Catch2 at the moment of writing this, so please share any suggestions or improvement ideas in the comments, I am looking forward to them.

NOTE: While writing this post, I was working with Bazel version 0.16.1 and Catch2 version 2.4.0.

Basic Bazel project

Let’s say we have following very simple cpp project that is built with Bazel build system.

Project structure:
WORKSPACE -> empty file
src/
  Money.hpp
  Money.cpp
  BUILD
src/Money.hpp:
#pragma once

class Money {
public:
  Money(const int amount);
  int amount() const;

private:
  const int _amount;
};
src/Money.cpp:
#include "Money.hpp"

Money::Money(const int amount) : _amount(amount) {}

int Money::amount() const {
  return this->_amount;
}
src/BUILD:
cc_library(
  name = "Money",
  srcs = ["Money.cpp"],
  hdrs = ["Money.hpp"]
)

In this project, we created only a simple class Money that is used to model certain amount of money.

We can build it by running bazel build //src:Money.

Adding Catch2

We want to add Catch2 test framework to our cpp project, to test if Money works correctly.

To do that, we do the following additions.

New project structure:
WORKSPACE
src/
  Money.hpp
  Money.cpp
  BUILD -> modified!
test/ -> new!
  BUILD
  main.cpp
  Money.test.cpp
  vendor/
    catch2/
      catch.hpp
      BUILD

Catch2 is header only library, which means we can just download the header file, add it to our project and then include it where we need it. I put it under test/vendor/catch2/, to make it clear it is dependency of tests, but also to make it clear it is third-party library. We need to define how is it built, so Bazel knows how to deal with it. Since it is header only library, it is very simple.

test/vendor/catch2/BUILD:
cc_library(
    name = "catch2",
    hdrs = ["catch.hpp"],
    visibility = ["//test:__pkg__"]
)

We added visibility attribute to make it usable in our tests.

In src/BUILD, we need to add visilibity attribute that tells Bazel to make this package visible in tests, which will enable us to add is dependency for related tests. This is how modified src/BUILD looks like now.

src/BUILD:
cc_library(
  name = "Money",
  srcs = ["Money.cpp"],
  hdrs = ["Money.hpp"],
  visibility = ["//test:__pkg__"]
)

Catch2 needs one translation unit with correctly defined main method. We define it in following way, with just 2 lines, as per their documentation.

test/main.cpp:
#define CATCH_CONFIG_MAIN
#include "catch.hpp"

We describe how to build tests in the BUILD file in test/ directory. We describe building of translation unit with main method, then describe specific tests, and finally we can describe test suites.

test/BUILD:
cc_library(
    name = "catch-main",
    srcs = ["main.cpp"],
    copts = ["-Itest/vendor/catch2"],
    deps = [
      "//test/vendor/catch2"
    ]
)

# Here we define our test. It needs to build together with the catch2 main that
# we defined above, so we add it to deps.
# We directly include src/Money.hpp and test/vendor/catch2/catch.hpp in
# Money.test.cpp, so we need to add their parent directories as copts.
# We also add Money and catch2 as dependencies.
cc_test(
    name = "Money",
    srcs = ["Money.test.cpp"],
    copts = ["-Itest/vendor/catch2/", "-Isrc/"],
    deps = [
        "//test/vendor/catch2", # Or "//test/vendor/catch2:catch2", it is the same.
        "catch-main",
        "//src:Money"
    ]
)

# Test suite that runs all the tests.
test_suite(
    name = "all-tests",
    tests = [
        "Money"
    ]
)

Finally, we write our test.

test/Money.test.cpp:
#include "catch.hpp"

#include "Money.hpp"

TEST_CASE("Money returns correct amount.") {
  const int moneyAmount = 5;
  const Money money(moneyAmount);
  REQUIRE(money.amount() == moneyAmount);
}

That is it! We can run our tests with bazel test //test:all-tests.

This will run tests through Bazel, which will hide Catch2 output. If you want to see it, you can run it with flag --test-output="all". However, formatting will not be exactly as it should be (e.g. there is no colors).

While Bazel team recommends running tests through Bazel, we can also skip it and run them directly if we wish, as Catch2 intended (output is nicer!). Since cc_case in Bazel creates binary, we can build specific test with e.g. bazel build //test:Money and then run it with ./bazel-bin/test/Money. However, this allows running a specific test, and not a test suite.

Extra

One thing I noticed is that there will be a lot of redundancy when defining specific tests in test/BUILD. Therefore, I defined the following Bazel macro.

test/macros.bzl:
# Given target name from //src package, expects .test.cpp file with same name.
# Creates a cc_test for that target.
def src_target_test(target_name):
    native.cc_test(
        name = target_name,
        srcs = [target_name + ".test.cpp"],
        copts = ["-Itest/vendor/catch2/", "-Isrc/"],
        deps = [
            "//test/vendor/catch2",
            "catch-main",
            "//src:" + target_name
        ]
    )

And then I load it and use it in test/BUILD.

test/BUILD:
load(":macros.bzl", "src_target_test")

cc_library(
    name = "catch-main",
    srcs = ["main.cpp"],
    copts = ["-Itest/vendor/catch2"],
    deps = [
      "//test/vendor/catch2"
    ]
)

# Individual tests (per target).
# Each test expects a target in src/ and a .test.cpp file with same name to exist.
# Add new tests here! Also, add them to test suite all-tests below.
src_target_test("Money")

# Test suite that runs all the tests.
test_suite(
    name = "all-tests",
    tests = [
        "Money"
    ]
)

Now it takes only one line to define new test (as long as it not more complicated/specific)!

Resources

I am reading book “Test Driven Development: By Example” by Kent Beck and I wanted to follow it along with project in C++. That is why I decided to use Bazel and Catch2, since it is a new project and I wanted to try new technologies.

At the moment of writing, state of the master branch in my public github repo in which I am doing this project is the same as described in this article. However, I do plan to improve it with time, so feel free to check its current state if you are reader from relatively far future.