Bazel C++ 20 project with (micro) unit testing framework, FakeIt, and nanobench
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.
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
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.
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.
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.
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.
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 mockingSomeInterface
, 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! ☀️🌞