Bazel C++ 20 project with (micro) unit testing framework, FakeIt, and nanobench

Create well well-structured Bazel C++ 20 project incorporating custom libraries like (micro) Unit Testing Framework or FakeIt. Dig in! πŸš€

Bazel C++ 20 project with (micro) unit testing framework, FakeIt, and nanobench

Previously, I wanted to experiment with Bazel and created a simple C++ project. If you are curious about how to set up such a project, feel free to read the following post.

Creating Bazel C++ project with Google Test & Google Benchmark
Check out this blog post on creating a Bazel C++ project with Google Test and Google Benchmark! πŸš€ Learn how to leverage these powerful tools for efficient testing and performance analysis in your projects.

The project layout was quite simple because I wanted to learn how quickly and easily I could get started with Bazel. Below is a summary of the structure I implemented for this evaluation.

project/
β”œβ”€β”€ benchmark/
    β”œβ”€β”€ ...
    └── ...
β”œβ”€β”€ lib
    β”œβ”€β”€ ...
    └── ...
β”œβ”€β”€ main/
    β”œβ”€β”€ ...
    └── ...
└── test/
    β”œβ”€β”€ ...
    └── ...

flattened project structure

The project was organized into distinct directories, each serving a specific purpose.

  • The benchmark directory housed benchmarks, providing a dedicated space for performance evaluations.
  • The lib directory was designated for the library components, encapsulating reusable functionalities.
  • The main directory focused on producing executables, representing the core application logic.
  • The test directory served as a dedicated space for housing and running tests, ensuring a systematic approach to testing the project's functionality.

Additionally, I incorporated frameworks like Google Test, Google Mock, and Google Benchmark to enhance testing, mocking, and benchmarking functionalities. Adding them to the project was trivial as Google provides frameworks with Bazel configuration out-of-the-box.

The primary drawback of a flat structure is that benchmarks and tests cover the entire project. Considering the project consists of one or more reusable libraries, it would be more logical to organize benchmarks and tests on a per-library basis, facilitating a more modular and scalable approach.

This time I wanted to refine the project structure and incorporate different frameworks.

Upgrading the structure

project/
β”œβ”€β”€ liba
    β”œβ”€β”€ benchmark/
        β”œβ”€β”€ ...
        └── ...
    β”œβ”€β”€ include/
        β”œβ”€β”€ ...
        └── ...
    β”œβ”€β”€ src/
        β”œβ”€β”€ ...
        └── ...
    └── test/
        β”œβ”€β”€ ...
        └── ...
β”œβ”€β”€ libb
    β”œβ”€β”€ benchmark/
        β”œβ”€β”€ ...
        └── ...
    β”œβ”€β”€ include/
        β”œβ”€β”€ ...
        └── ...
    β”œβ”€β”€ src/
        β”œβ”€β”€ ...
        └── ...
    └── test/
        β”œβ”€β”€ ...
        └── ...
β”œβ”€β”€ main/
    └── src/
        β”œβ”€β”€ main.cc
        └── ...
└── third_party/
    β”œβ”€β”€ ...
    └── ...

upgraded project structure

The updated project structure is segmented into discrete libraries like liba, libb, etc. The main directory focuses on executable components, and third_party is reserved for external libraries. Now, delving into each library's structure, we find four key directories. The benchmark directory is allocated for performance evaluations, include for header files, src for source code, and test for comprehensive testing.

While the project is decomposed into multiple libraries, each library requires its own BUILD file. Let's see one example.

load("@rules_cc//cc:defs.bzl", "cc_library")

cc_library(
    name = "lib",
    srcs = glob(["src/*.cc"]),
    hdrs = glob(["include/*.h"]),
    includes = ["include"],
    visibility = ["//visibility:public"],
)

cc_test(
    name = "test",
    srcs = glob(["test/*.cc"]),
    deps = [
        ":lib",
        "@com_github_boost_ext_ut//:unittest",
        "@com_github_eranpeer_fakeit//:fakeit",
    ],
)

cc_test(
    name = "benchmark",
    srcs = glob(["benchmark/*.cc"]),
    deps = [
        ":lib",
        "//third_party/nanobench",
        "@com_github_boost_ext_ut//:unittest",
    ],
)

library BUILD file

This Bazel BUILD file configures a C++ library named lib utilizing the cc_library rule. The library incorporates source files from the src directory and header files from include, designed for public visibility. Two executable test targets, test and benchmark, are defined with the cc_test rule. Previously chosen libraries are substituted with alternative choices:

  • Google Test was replaced with a single-header, macro-free (micro) unit testing framework,
  • Google Mock gave way to FakeIt, a straightforward mocking framework for C++, and
  • Google Benchmark was swapped for nanobench, a single-header microbenchmarking tool compatible with C++11/14/17/20.

The chosen libraries require the creation of dedicated BUILD files to enable their seamless inclusion and compilation within the project. Let's see how to do this using the example of a (micro) unit testing framework.

To allow the testing framework to compile, we have to fetch the repository first.

...

git_repository(
    name = "com_github_boost_ext_ut",
    build_file = "//third_party/unittest:BUILD",
    remote = "https://github.com/boost-ext/ut",
    tag = "v2.0.1",
)

...

WORKSPACE file

The repository is identified with the name com_github_boost_ext_ut and linked to the BUILD file located at //third_party/unittest:BUILD. The remote parameter points to the boost-ext.ut repository on GitHub (https://github.com/boost-ext/ut), and the specific version tagged as v2.0.1 is selected. This rule effectively integrates the ut library into the project, allowing Bazel to manage its inclusion and dependencies during the build process.

Once the WORKSPACE file is updated, we must define how to compile the fetched library. Therefore, we must create a BUILD file.

load("@rules_cc//cc:defs.bzl", "cc_library")

cc_library(
    name = "libunittest",
    hdrs = [
        "include/boost/ut.hpp",
    ],
    includes = ["include"],
)

cc_library(
    name = "unittest",
    visibility = ["//visibility:public"],
    deps = [
        "libunittest",
        "@//misc/empty_main:empty_main",
    ],
)

third_party/unittest/BUILD file

The cc_library rule is employed to define two C++ libraries. The first library, named libunittest, encompasses the boost-ext.ut header file include/boost/ut.hpp and designates the include directory for header file inclusion. The second library, named unittest, is intended for public visibility and depends on the libunittest library and an external dependency provided by the @//misc/empty_main:empty_main label. The public-facing unittest library is used when writing tests and creating testing executables.

Similarly, we have to incorporate repositories into the WORKSPACE file for the remaining libraries and create BUILD files.

Building the project

When building a project, having a working toolchain is a must, so why not download it and set it up on the first run?

...
http_archive(
    name = "toolchains_llvm",
    canonical_id = "0.10.3",
    sha256 = "b7cd301ef7b0ece28d20d3e778697a5e3b81828393150bed04838c0c52963a01",
    strip_prefix = "toolchains_llvm-0.10.3",
    url = "https://github.com/grailbio/bazel-toolchain/releases/download/0.10.3/toolchains_llvm-0.10.3.tar.gz",
)

load("@toolchains_llvm//toolchain:deps.bzl", "bazel_toolchain_dependencies")

bazel_toolchain_dependencies()

load("@toolchains_llvm//toolchain:rules.bzl", "llvm_toolchain")

llvm_toolchain(
    name = "llvm_toolchain",
    llvm_version = "16.0.0",
)

load("@llvm_toolchain//:toolchains.bzl", "llvm_register_toolchains")

llvm_register_toolchains()
...

In the WORKSPACE file, the above code utilizes the http_archive rule to fetch the LLVM toolchain extension version 0.10.3 from a specified GitHub release. The downloaded archive is verified using its SHA256 checksum, and the extension is appropriately extracted and striped from the prefix. Subsequently, the bazel_toolchain_dependencies rule is loaded and invoked. Then llvm_toolchain rule is loaded and invoked to define a Bazel toolchain named llvm_toolchain with version 16.0.0. Finally, the llvm_register_toolchains rule is loaded to register the LLVM toolchains. The registered toolchain is downloaded upon the first run.

Sample tests

The last thing I wanted to evaluate was the (micro) unit testing framework.

#include "word_generator.h"

#include <boost/ut.hpp>
#include <fakeit.hpp>
#include <string>

namespace sandbox {
using namespace boost::ut;
using namespace fakeit;

struct SomeInterface {
  virtual int foo(int) = 0;
  virtual int bar(std::string) = 0;
};

suite hello_world_test = [] {
  "hello"_test = [] {
    auto generator = sandbox::create_word_generator();
    expect(eq(generator->next(), std::string_view{"Hello, world!"}));
  };

  "mock"_test = [] {
    Mock<SomeInterface> mock;
    When(Method(mock, foo)).Return(1);
    SomeInterface& i = mock.get();
    expect(eq(i.foo(0), 1));
  };
};
}  // namespace sandbox

sample tests

Along with the (micro) unit testing framework, FakeIt is employed for testing as well. The above code defines the hello_world_test suite. It incorporates two tests:

  • the hello test validates a word generator's functionality, ensuring the correct string output,
  • while the mock test demonstrates the use of FakeIt for mocking SomeInterface, showcasing expectation setting and method call verification.

Notably, the unit testing framework lacks any macros, while the FakeIt framework doesn't necessitate the pre-definition of mocks before their usage!

Summary

Choosing the right project layout is essential because it enhances modularity. Bazel facilitates the incorporation of not only Google's provided libraries and frameworks but also custom ones. Utilizing custom Bazel plugins enables the automatic downloading of toolchains for project compilation. Additionally, (micro) Unit Testing Framework and FakeIt provide extensive testing capabilities, often without the use of cumbersome macros and the necessity to predefine mocks.

Grab the GitHub link and enjoy! β˜€οΈπŸŒž

GitHub - jakub-k-slys/bazel-cpp20: C++ 20 template using bazel, clang, (micro) unit testing framework, FakeIt, and nanobench.
C++ 20 template using bazel, clang, (micro) unit testing framework, FakeIt, and nanobench. - GitHub - jakub-k-slys/bazel-cpp20: C++ 20 template using bazel, clang, (micro) unit testing framework, F…

References

GitHub - jakub-k-slys/bazel-cpp-sandbox
Contribute to jakub-k-slys/bazel-cpp-sandbox development by creating an account on GitHub.
ut
UT: C++20 ΞΌ(micro)/Unit Testing Framework
GitHub - eranpeer/FakeIt: C++ mocking made easy. A simple yet very expressive, headers only library for c++ mocking.
C++ mocking made easy. A simple yet very expressive, headers only library for c++ mocking. - GitHub - eranpeer/FakeIt: C++ mocking made easy. A simple yet very expressive, headers only library for…
nanobench β€” nanobench documentation
GoogleTest User’s Guide
GoogleTest - Google Testing and Mocking Framework
gMock for Dummies
GoogleTest - Google Testing and Mocking Framework
Benchmark
A microbenchmark support library