Generating C++ code coverage metrics using GoogleTest and LCOV
Prerequisites
LCOV
needs to be installed using:
# CentOS / Redhat
sudo yum -y install lcov
# Debian / Ubuntu
sudo apt-get -y install lcov
Example project layout
The example in this article uses a project layout as shown below:
project_dir
│ app.cpp
│ Makefile
├───src
│ └───app
│ coordinate.cpp
│ coordinate.hpp
│ coordinate2.cpp
│ coordinate2.hpp
└───test
coordinate2_test.cpp
coordinate_test.cpp
main_test.cpp
Example makefile
This makefile performs all the operations required to build, test, and generate coverage reports.
This makefile should be transferable to most basic C++ projects - just update the TARGET
, OBJFILES
, and TESTOBJFILES
variables. For more complex projects the steps performed in the coverage
section should still be very similar.
TARGET = app
BUILD_DIR = build
SRC_DIR = src/$(TARGET)
LIB_DIR = lib
TEST_DIR = test
OBJFILES = coordinate.o coordinate2.o
TESTOBJFILES = coordinate_test.o coordinate2_test.o main_test.o
GIT_REPO = https://github.com
GTEST_LD = -L $(LIB_DIR)/googletest/build/googlemock/gtest/ -l gtest -l pthread
GTEST_INC_DIR = $(LIB_DIR)/googletest/googletest/include/
GTEST_DEP = $(LIB_DIR)/googletest/build/googlemock/gtest/libgtest.a
CXX = g++
CXXFLAGS = -g -Wall -std=c++11 -I ./src -I ./lib/inih -I $(GTEST_INC_DIR)
all: app tests
app: $(BUILD_DIR)/$(TARGET)
# Build Application
$(BUILD_DIR)/$(TARGET): $(OBJFILES:%.o=$(BUILD_DIR)/%.o) $(TARGET).cpp
@mkdir -p $(BUILD_DIR)
$(CXX) $(CXXFLAGS) $^ -o $(BUILD_DIR)/$(TARGET)
# Catch all rule for building objects
$(BUILD_DIR)/%.o : $(SRC_DIR)/%.cpp $(SRC_DIR)/%.hpp
@mkdir -p $(BUILD_DIR)
$(CXX) -c $(CXXFLAGS) $< -o $@
# Build Tests
tests: $(GTEST_DEP) $(BUILD_DIR)/test/$(TARGET)_test
$(BUILD_DIR)/test/$(TARGET)_test : $(TESTOBJFILES:%.o=$(BUILD_DIR)/test/%.o) $(OBJFILES:%.o=$(BUILD_DIR)/%.o)
$(CXX) $(CXXFLAGS) $^ $(GTEST_LD) -o $(BUILD_DIR)/test/$(TARGET)_test
# Catch all rules for building test objects
$(BUILD_DIR)/test/%_test.o: $(TEST_DIR)/%_test.cpp
@mkdir -p $(BUILD_DIR)/test
$(CXX) -c $(CXXFLAGS) $< -o $@
# Run tests
run_tests: tests
$(BUILD_DIR)/test/$(TARGET)_test
$(GTEST_DEP) :
@echo "Downloading googletest dependency..."
@rm -rf $(LIB_DIR)/googletest
@mkdir -p $(LIB_DIR)
# Release 1.8.1 is the last version that works on CentOS 7
cd $(LIB_DIR); git clone $(GIT_REPO)/google/googletest.git -b release-1.8.1
cd $(LIB_DIR)/googletest; mkdir build; cd build; cmake3 ..; make;
coverage : CXXFLAGS += --coverage
coverage : clean run_tests
@echo "Rebuild and run tests with coverage enabled"
lcov --directory ./build/ --no-recursion -c -o app_coverage.info
lcov --remove app_coverage.info "/usr*" -o app_coverage.info
genhtml -o build/coverage/ -t "Application Coverage Report" --legend --num-spaces 4 app_coverage.info
rm app_coverage.info
make clean
clean:
$(RM) $(BUILD_DIR)/$(TARGET) $(BUILD_DIR)/test/$(TARGET)_test $(BUILD_DIR)/*.o $(BUILD_DIR)/test/*.o
$(RM) $(BUILD_DIR)/*.gcno $(BUILD_DIR)/*.gcda $(BUILD_DIR)/*/*.gcno $(BUILD_DIR)/*/*.gcda
clean_all: clean
$(RM) -rf $(BUILD_DIR)
$(RM) -rf $(LIB_DIR)
.PHONY: clean clean_all all tests app run_tests coverage
Note that the version of GoogleTest is limited to 1.8.1 to ensure compatibility with CentOS 7 (C++11). The Makefile should still work if it is updated to use a newer version of GoogleTest and C++.
The following commands can be used:
# Compile the application
make app
# Run all GoogleTest unit tests
make run_tests
# Build the project with coverage enabled, run the
# unit tests, generate coverage reports, and clean up.
make coverage
Note that make coverage
first performs a clean build. This is because generating coverage reports requires the code to be compiled with the gcc --coverage
option which is not used for the default build.
The after running make coverage
the coverage report is located as an HTML webpage in build/coverage/
Example project files
The following files are just included as a reference of a working example project for the above Makefile. Two classes are included (actually two copies of the same class but with different names). One class has 100% unit test coverage and the other has partial coverage. Note that this code doesn't do anything useful!
app.cpp
#include <iostream>
#include <string>
#include <app/coordinate.hpp>
int main(int argc, char **argv) {
Coordinate x(0.100, -80.0);
return EXIT_SUCCESS;
}
src/app/coordinate.cpp
#include <stdexcept>
#include <app/coordinate.hpp>
Coordinate::Coordinate(double lat, double lng) : lat(lat), lng(lng) {
if (lat > 90.0 || lat < -90.0 || lng > 180.0 || lng < -180.0) {
throw std::runtime_error("ERROR: Failed to parse coordinate coordinate. Value out of range");
}
}
bool Coordinate::operator==(const Coordinate &other) const {
return (lat == other.lat && lng == other.lng);
}
bool Coordinate::operator!=(const Coordinate &other) const {
return !(*this == other);
}
void Coordinate::Offset(const Coordinate &other) {
lat += other.lat;
lng += other.lng;
}
src/app/coordinate.hpp
#pragma once
class Coordinate {
private:
public:
Coordinate(double lat, double lng);
bool operator==(const Coordinate &other) const;
bool operator!=(const Coordinate &other) const;
void Offset(const Coordinate &other);
double lat;
double lng;
};
src/app/coordinate2.cpp
Note that the Coordinate2
class is just a renamed duplicate of the Coordinate
class.
#include <stdexcept>
#include <app/coordinate2.hpp>
Coordinate2::Coordinate2(double lat, double lng) : lat(lat), lng(lng) {
if (lat > 90.0 || lat < -90.0 || lng > 180.0 || lng < -180.0) {
throw std::runtime_error("ERROR: Failed to parse coordinate coordinate. Value out of range");
}
}
bool Coordinate2::operator==(const Coordinate2 &other) const {
return (lat == other.lat && lng == other.lng);
}
bool Coordinate2::operator!=(const Coordinate2 &other) const {
return !(*this == other);
}
void Coordinate2::Offset(const Coordinate2 &other) {
lat += other.lat;
lng += other.lng;
}
src/app/coordinate2.hpp
#pragma once
class Coordinate2 {
private:
public:
Coordinate2(double lat, double lng);
bool operator==(const Coordinate2 &other) const;
bool operator!=(const Coordinate2 &other) const;
void Offset(const Coordinate2 &other);
double lat;
double lng;
};
test/main_test.cpp
#include <gtest/gtest.h>
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
test/coordinate_test.cpp
Note: Basic unit tests cover all methods in the Coordinate
class.
#include <gtest/gtest.h>
#include <app/coordinate.hpp>
TEST(Coordinate, DoubleConstrutor) {
Coordinate coordinate = Coordinate(-43.454,89.0454);
EXPECT_DOUBLE_EQ(-43.454, coordinate.lat);
EXPECT_DOUBLE_EQ(89.0454, coordinate.lng);
}
TEST(Coordinate, DoubleConstrutorOutOfRange) {
EXPECT_THROW({
Coordinate coordinate = Coordinate(90.100,50.0);
}, std::runtime_error);
EXPECT_THROW({
Coordinate coordinate = Coordinate(-90.1,50.0);
}, std::runtime_error);
EXPECT_THROW({
Coordinate coordinate = Coordinate(45.0,180.1);
}, std::runtime_error);
EXPECT_THROW({
Coordinate coordinate = Coordinate(45.0,-180.1);
}, std::runtime_error);
}
TEST(Coordinate, EqualOverload) {
Coordinate a = Coordinate(10.1,-34.2);
Coordinate b = Coordinate(10.1,-34.2);
Coordinate c = Coordinate(10.1,34.2);
EXPECT_TRUE(a == b);
EXPECT_FALSE(a == c);
}
TEST(Coordinate, NotEqualOverload) {
Coordinate a = Coordinate(-48.0,90.0);
Coordinate b = Coordinate(-48.0,90.0);
Coordinate c = Coordinate(10.1,34.2);
EXPECT_FALSE(a != b);
EXPECT_TRUE(a != c);
}
TEST(Coordinate, OffsetValid) {
Coordinate a = Coordinate(10.0,-10.0);
Coordinate b = Coordinate(0.1, 0.2);
a.Offset(b);
EXPECT_DOUBLE_EQ(10.1, a.lat);
EXPECT_DOUBLE_EQ(-9.8, a.lng);
}
test/coordinate2_test.cpp
Note: no unit test for the Offset
method in the Coordinate2
class.
#include <gtest/gtest.h>
#include <app/coordinate2.hpp>
TEST(Coordinate2, DoubleConstrutor) {
Coordinate2 coordinate = Coordinate2(-43.454,89.0454);
EXPECT_DOUBLE_EQ(-43.454, coordinate.lat);
EXPECT_DOUBLE_EQ(89.0454, coordinate.lng);
}
TEST(Coordinate2, DoubleConstrutorOutOfRange) {
EXPECT_THROW({
Coordinate2 coordinate = Coordinate2(90.100,50.0);
}, std::runtime_error);
EXPECT_THROW({
Coordinate2 coordinate = Coordinate2(-90.1,50.0);
}, std::runtime_error);
EXPECT_THROW({
Coordinate2 coordinate = Coordinate2(45.0,180.1);
}, std::runtime_error);
EXPECT_THROW({
Coordinate2 coordinate = Coordinate2(45.0,-180.1);
}, std::runtime_error);
}
TEST(Coordinate2, EqualOverload) {
Coordinate2 a = Coordinate2(10.1,-34.2);
Coordinate2 b = Coordinate2(10.1,-34.2);
Coordinate2 c = Coordinate2(10.1,34.2);
EXPECT_TRUE(a == b);
EXPECT_FALSE(a == c);
}
TEST(Coordinate2, NotEqualOverload) {
Coordinate2 a = Coordinate2(-48.0,90.0);
Coordinate2 b = Coordinate2(-48.0,90.0);
Coordinate2 c = Coordinate2(10.1,34.2);
EXPECT_FALSE(a != b);
EXPECT_TRUE(a != c);
}