Bazel Setup for unit testing and debugging C++ Applications with Visual Studio Code


If you write an application you don’t just want a hello-world to run, but also to test and debug it. In this little example I show a basic setup for visual studio to be able to start your own project. I will also explain the configuration, so you can make changes according to your needs.

The folder structure is pretty simple. For a more detailed explanation please have a look at the official documentation.

├─.vscode
│ ├─launch.json
│ └─tasks.json

├─myapplication
│ ├─BUILD.bazel
│ └─main.cpp

├─mylibrary
│ ├─BUILD.bazel
│ ├─lib.h
│ └─lib.cpp

└─MODULE.bazel

Dependencies

Dependencies are defined in the MODULE.bazel file. In our case its just the testing library from google.

bazel_dep(name = "googletest", version = "1.15.2")

The Library

We just need a simple method to test our library. For this demo project I decided to just return my name. The header file lib.h and the implementation lib.cpp look like this.

#ifndef LIB_H_
#define LIB_H_

#include<string>

std::string getName();

#endif
#include "mylibrary/lib.h"
#include<string>

std::string getName() {
  return "Florian";
}

The test.cpp file defines a simple test that checks if the returned name is correct an no error is thrown.

#include "gtest/gtest.h"
#include "lib.h"

TEST(Lib, getName) {
  EXPECT_EQ("Florian", getName());
}

The BUILD.bazel file describes what is included in the library and also how it is tested.

cc_library(
    name = "lib",
    srcs = ["lib.cpp"],
    hdrs = ["lib.h"],
    visibility = ["//myapplication:__pkg__"]
)

cc_test(
    name = "test",
    srcs = ["test.cpp"],
    deps = [
        "@googletest//:gtest",
        "@googletest//:gtest_main",
        ":lib",
    ],
    copts = [
        "-std=c++20"
    ],
)

It starts with the library definition cc_library and includes the name, the source srcs and header hdrs files. It also includes the own visibility. We need the library to be visible in our application and therefore include it in the visibility definition.

The more interesting part ist the test definition cc_test. It also includes the name and source files srcs, but also the test dependencies deps. Here we have to include the google test libraries and also our own library.

We can run the tests with the following command while being in the root folder.

bazel test mylibrary:test

The Application

The code for the application is even simpler. We just need to include our library and print the name. This is done in main.cpp.

#include <iostream>
#include "mylibrary/lib.h"

int main() {
    std::cout << "Hello " << getName() << std::endl;
    return 0;
}

The corresponding BUILD.bazel contains only already known fields.

cc_binary(
    name = "main",
    srcs = ["main.cpp"],
    deps = [
        "//mylibrary:lib",
    ],
)

To build and execute the application we can run the following command.

bazel run myapplication:main

Visual Studio Code Configuration

So far, we just did a basic setup which is already described in more detail in the bazel docs. However, now we want to integrate our project in visual studio, so we can start and debug the application.

First, we need to define tasks to build the test and the application with debug information. This is done in the tasks.json file within the hidden .vscode directory.

{
    "tasks": [
        {
            "label": "Build Bazel (Debug)",
            "type": "shell",
            "command": [
                "bazel build //myapplication:main -c dbg"
            ],
            "group": "build"
        },
        {
            "label": "Build Bazel Test (Debug)",
            "type": "shell",
            "command": [
                "bazel build //${relativeFileDirname}:test -c dbg"
            ],
            "group": "build"
        }
    ],
    "version": "2.0.0"
}

The tasks array contains a list of tasks. Each task can be called by other scripts. Each task has a label, a type and a command. The label is needed to call the task and should be self explanatory. To build the application we need to execute the bazel build. This is done in the shell with the bazel build command. Important here is to use the dbg flag to include debug information.

For the tests we use the placeholder relativeFileDirname. This placeholder will contain the current folder of the selected test file. This is a nice trick to only build the tests you are currently working on. If the project gets more complicated this is maybe not feasible anymore. Feel free to replace the path with //... to always build all targets.

As a last step we need launch configurations. Those are defined in the launch.json file.

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "cppdbg",
            "request": "launch",
            "name": "Debug Test",
            "program": "${workspaceFolder}/bazel-bin/${relativeFileDirname}/test",
            "args": [],
            "cwd": "${workspaceFolder}",
            "preLaunchTask": "Build Bazel Test (Debug)",
        },
        {
            "type": "cppdbg",
            "request": "launch",
            "name": "Debug Run",
            "program": "${workspaceFolder}/bazel-bin/myapplication/main",
            "args": [],
            "cwd": "${workspaceFolder}",
            "preLaunchTask": "Build Bazel (Debug)",
        }
    ]
}

The type cppdbg is for launch configuration with the gnu project debugger. More is described in the microsoft docs. This can also be changed to lldb for example. The important part hier is the preLaunchTask which describes which task is executed before launch. Here we need to call our tasks to compile the application first.

The program field describes which application is called for debugging. Here we call either the main application or one of the tests with the above describe placeholder relativeFileDirname.

To run a test selected the desired file and start the Debug Test launch configuration.