I’m starting a new project in C++, but I’ve run into a couple of questions before starting:
-
Which build system should I use?
-
Which unit testing framework?
Tip
|
If you just want project template so you can have a C++ project skeleton ready in seconds, just go to https://github.com/carlosvin/uuid-cpp and follow the instructions in README.md. |
Choosing Build System (Meson)
I have used before Make, Maven, Scons, Gradle and Autotools, but I have some reasons to try find something else:
- Autotools
-
It is not easy to configure and maintain. There are several configuration files and several configuration steps.
- Gradle
-
CPP feature is still incubating. Not very fast. You can check a similar example project at Build C++ project with Gradle.
- Make
-
I don’t love the syntax. Files tends to get messy as project grows.
- Scons
-
It is just slower and not as easy to understand than Meson.
- Maven
-
It is slow and you might end up "Javatizing" your C++ project structure.
Note
|
I’ve listed just things I don’t like, those projects have other great features. |
CMake vs Meson
Although Meson is written in Python, it generates a Ninja build project. First time you configure the project you have to run Meson, but for building or testing you are actually running Ninja.
CMake is also able to generate Ninja files among other formats, check CMake generators documentation for more information.
- CMake
-
It has a big advantage over Meson, it is mature and widely used in many projects, which means there are many examples and it will fulfill your C++ project building needs.
- Meson
-
It is a young project compared with CMake, but it is growing quite fast and it has been adopted in other big projects like Gnome, they have an initiative to port from Autotools to Meson.
Finally I’ve chosen Meson because syntax is really clear to me, when I read meson.build
file I can quickly understand what is happening during build process.
$ meson build . (1)
$ cd build
$ ninja build (2)
$ ninja test (3)
-
First time you configure the project
-
Each time you build it
-
Each time you run tests
Unit Testing Framework
I have used some xUnit based libraries like UnitTest++, CppUTest or Google Test which match perfectly with Google Mock. If you want a safe bet that fulfills almost of your testing needs I highly recommend Google Test.
But time ago I found a testing framework with some interesting features, Catch:
-
It is just a header file with no external dependencies, so very easy to start (wget + include downloaded file).
-
You can use normal unit test style or BDD-style
If you want to know more about Catch, I recommend you to give it a try, it is a matter of 2 minutes to have a simple example up and running. You can also read some interesting articles like Why do we need yet another C test framework?] or https://blog.coldflake.com/posts/Testing-C-with-a-new-Catch/[Testing C++ With A New Catch.
doctest: A Catch alternative
There is another testing framework named doctest, with same benefits as Catch, but it promises to be faster and lighter (performance results) than Catch.
doctest is modeled after Catch and some parts of the code have been taken directly, but there are differences.
It hasn’t been easy to decide, both are really similar, following you can see differences:
@@ -1,12 +1,12 @@
-#define CATCH_CONFIG_MAIN // It tells Catch to provide a main() - only do this in one cpp file
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
-#include "catch.hpp"
+#include "doctest.h"
#include "Uuid.h"
#include <string>
constexpr int MAX_ITERS = 100;
-TEST_CASE("Uuid", "[uuid]")
+TEST_CASE("Uuid")
{
for (int i = 0; i < MAX_ITERS; i++)
{
@@ -26,7 +26,7 @@ TEST_CASE("Uuid", "[uuid]")
// BDD style
-SCENARIO("UUID creation", "[Uuid]")
+SCENARIO("UUID creation")
{
GIVEN("A random UUID ")
I’ve finally chosen doctest because it promises to be faster: performance results.
Note
|
I’ve created project using both frameworks you can find them in corresponding branches: doctest branch or catch branch. |
Example
I’ve created an example to illustrate this article: https://github.com/carlosvin/uuid-cpp.
It is a basic implementation of UUID pseudo-random generator based on mt19937 which is not cryptographically secure.
Project output artifacts
When we install the project using Meson (Ninja), we will get some artifacts generated and copied in our system.
-
Shared library:
libuuid
. -
Header library for developers who want to use the shared library:
include/Uuid.h
. -
Executable
uuidgen
(UUID generator). -
Test executable (not installed). It tests shared library.
For example, if you execute ninja install
on Linux, you will get something like:
/usr/local/lib/libuuid.so
/usr/local/include/Uuid.h
/usr/local/bin/uuidgen
Project structure (Fork project)
- meson.build
-
Root project file configuration. It defines project properties and subdirectories.
project(
'cpp-meson-example', # project name
'cpp', # C++ project, e.g: for C project
version : '1.0.0',
license : 'MIT',
default_options : ['cpp_std=c++11']) # compile for C++
# it will be referred from subdir projects
inc = include_directories('include')
# meson will try to find a meson.build file inside following directories
subdir('include')
subdir('src')
subdir('test')
- include
-
- meson.build
-
Build configuration file for include directory.
# Select header files to be installed
install_headers('Uuid.h')
- Uuid.h
-
Header file, it is the library interface definition which will be included from projects using that library
namespace ids {
class Uuid {
private:
// ...
- src
-
- meson.build (src)
-
It declares 2 output artifacts, library
libuuid
and executableuuidgen
. Executable depends on the libary, it will use the libary to generate UUID.
libuuid = shared_library(
'uuid', (1)
'Uuid.cpp', (2)
include_directories : inc, (3)
install : true) (4)
uuidgen = executable(
'uuidgen', (5)
'main.cpp', (6)
include_directories : inc, (7)
link_with : libuuid, (8)
install : true) (9)
-
library name
-
source files to be compile
-
previously declared include directories in root
meson.build
-
libuuid
will be part of project installation -
executable name
-
source files to compile
-
previously declared include directories in root
meson.build
-
linking executable with shared previously declared shared library
libuuid
-
uuidgen
executable be part of project installation- main.cpp
-
Entry point for main executable
uuidgen
.
#include "Uuid.h"
#include <iostream>
int main()
{
ids::Uuid uuid;
std::cout << uuid.to_str() << std::endl;
return 0;
}
- Uuid.cpp
-
Implementation of declared class in header file.
#include "Uuid.h"
Uuid::Uuid()
{ // ...
- test
-
- meson.build (test)
-
File to configure tests build process.
testexe = executable(
'testexe', (1)
'uuid_test.cpp', (2)
include_directories : inc, (3)
link_with : libuuid) (4)
test('Uuid test', testexe) (5)
test('Uuid test with args and env', testexe, args : ['arg1', 'arg2'], env : ['FOO=bar']) (6)
-
test executable name
-
tests source files to be compiled
-
declared include directories in root
meson.build
-
link test executable with previously declared shared library
libuuid
-
test execution
-
we can specify other test execution passing arguments or environment variables
cd test
wget https://raw.githubusercontent.com/onqtam/doctest/master/doctest/doctest.h
- uuid_test.cpp
-
Tests implementation.
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include "doctest.h"
#include "Uuid.h"
#include <string>
constexpr int MAX_ITERS = 100;
TEST_CASE("Uuid")
{
for (int i = 0; i < MAX_ITERS; i++)
{
ids::Uuid uuid;
std::string uuid_str{uuid.to_str()};
INFO(uuid_str);
// If assertion fails test execution is stopped
REQUIRE(uuid_str.size() == 36);
// If assertion fails test execution continues
CHECK(uuid.most > 0);
CHECK(uuid.least > 0);
}
}
// BDD style
SCENARIO("UUID creation")
{
GIVEN("A random UUID ")
{
ids::Uuid uuid;
std::string uuid_str{uuid.to_str()};
REQUIRE(uuid_str.size() == 36);
WHEN("get the most and least")
{
THEN("should be more than 0")
{
CHECK(uuid.most > 0);
CHECK(uuid.least > 0);
}
}
}
}
Tip
|
You can find how to build and test the example project at: https://github.com/carlosvin/uuid-cpp#how-to-build-the-example |