Concepts
Google Test (a.k.a GTest) is an open source unit testing structure. From the official website
- Test Suite vs Test Case: A test suite contains one or many tests. You should group your tests into test suites that reflect the structure of the tested code. When multiple tests in a test suite need to share common objects and subroutines, you can put them into a test fixture class.
Some common macros include:
EXPECT_EQ
vs ASSERT_EQ
: EXPECT_*
versions generate nonfatal failures, which don’t abort the current function. Usually EXPECT_*
are preferred, as they allow more than one failure to be reported in a test. However, you should use ASSERT_*
if it doesn’t make sense to continue when the assertion in question fails.
Usage
- Install GTest if your environment doesn’t have it
1
2
3
4
5
6
| git clone https://github.com/google/googletest
cd googletest
mkdir -p build install
cd build
cmake -DCMAKE_INSTALL_PREFIX=../install .. Install locally
make -j32 install
|
Simple Test Case
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Test function
double getFrobeniusNorm(const cv::Mat &m){
// element-wise matrix multiplication
// cv::sum sums of elements across all dimensions
return std::sqrt(cv::sum(m.mul(m))[0]);
}
// Test case
#include <gtest/gtest.h>
TEST(CvUtilsTest, TestFrobeniusNorm) {
cv::Mat_<double> m (3, 3);
m << 1.0, 2.0, 3.0,
0.0, 0.0, 0.0,
1.0, 2.0, 0.0;
double norm = getFrobeniusNorm(m);
EXPECT_EQ(norm, std::sqrt(1 + 4 + 9 + 1 + 4));
}
|
Test Fixture
Test fixture is a mechanism to share code between test cases in a test suite. One common misconception is “the text fixture will be reused across tests”. THAT IS NOT TRUE! Test fixtures are only for sharing initialization code. Below, we have an example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| class MyTestFixture : public ::testing::Test{
protected:
void SetUp() override {
str_ = "Hello World";
std::cout<<str_<<std::endl;
}
void TearDown() override {
// For extra destruction work
}
static void SetUpTestSuite(){
// Initialize shared resource (like database access) for the entire test suite
}
static void TearDownTestSuite(){
// Destruct test suite's shared resources
}
std::string str_;
};
TEST_F(MyTestFixture, SomeTest){
}
|
- Note, to use a test fixture, one needs
TEST_F
instead of TEST
.
-
Another option is to use constructors. The official documentation has a good recommendation on when to use which.. The TL;DR is:
- Use ctor when we want to initialize
const
member variables. If we know they wouldn’t change throughout the tests, it’d be a good practice to do so.
- Use
SetUp()
when:
- We need to call a virtual function during initialization. See this article for more about the rationale.
- If a fatal failure could happen,
ASSERT_*
is a good choice for that purpose, but it cannot be used in ctor or dtor and can be only used in SetUp()
SetUp()
can catch exceptions. Arguably, it could be a good idea if you don’t want uncaught exceptions to interrupt your normal tests (e.g., an emergency test fix)
Test Environment
Test environment helps initialize test resource that’s shared for the entire test binary, e.g., logging system initialization. One potential bug is this environment could be shared across multiple compilation units, if they comprise the same binary.
1
2
3
4
5
6
7
8
9
10
11
12
| class Environment : public ::testing::Environment{
public:
~Environment() override {}
void SetUp() override {std::cout<<"This is intialized only once globally"<<std::endl;}
void TearDown() override {std::cout<<"This is destructed only once globally"<<std::endl;}
};
int main(int argc, char **argv){
testing::AddGlobalTestEnvironment(new Environment);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
|
General Gtest with CMake
If using CMakeLists.txt
, we can create a test executable for each of them:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Test files
file(GLOB_RECURSE TEST_FILES "tests/unit/*cpp")
foreach(TEST_FILE ${TEST_FILES})
# NAME_WE means "name without extension"
get_filename_component(TEST_EXEC_NAME ${TEST_FILE} NAME_WE)
add_executable(${TEST_EXEC_NAME}_test ${TEST_FILE})
add_test(NAME ${TEST_EXEC_NAME}_test COMMAND ${TEST_EXEC_NAME}_test)
target_link_libraries(${TEST_EXEC_NAME}_test
${PROJECT_NAME}_dependencies
gtest
gtest_main
Threads::Threads
)
endforeach()
enable_testing()
|
add_test()
does NOT create the test executable, it simply registers the exectuble as a test.
Threads::Threads
must be added to the above gtest. Otherwise, the gtest will fail silently, which is really annoying!!)
Gtest For ROS
The Snippet in section “General Gtest with CMake” already can be built by catkin:
1
2
3
4
5
6
| # build all executables
catkin build
# Run tests
catkin test
# Or for a specific package
catkin test <PACKAGE>
|
- The test executable is in
devel/lib/${PACKAGE_NAME}
- Upon failures,
catkin test
could throw a failure “double freeing” if we do not have a main() in our tests and uses gtest_main. This wouldn’t arise if we run the executable alone.
So, my best guess is this is a memory-freeing bug in catkin test
with gtest_main
. But if we add a main ourselves, and uses catkin_add_gtest()
, we should be all good.
catkin test
requires roscore to be running. If it’s not run, it will show as stuck in the building phase.
- The CMakeLists.txt is as below:
1
2
3
4
5
6
7
8
9
10
| file(GLOB_RECURSE TEST_FILES "tests/unit/*cpp")
foreach(TEST_FILE ${TEST_FILES})
# NAME_WE means "name without extension"
get_filename_component(TEST_EXEC_NAME ${TEST_FILE} NAME_WE)
catkin_add_gtest(${TEST_EXEC_NAME}_test ${TEST_FILE})
target_link_libraries(${TEST_EXEC_NAME}_test
${catkin_LIBRARIES}
)
endforeach()
enable_testing()
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #include <ros/ros.h>
#include <gtest/gtest.h>
TEST(TestFeatureDetection, TestORB){
EXPECT_EQ(1,2);
}
int main(int argc, char **argv){
testing::InitGoogleTest(&argc, argv);
ros::init(argc, argv, "tester");
ros::NodeHandle nh;
return RUN_ALL_TESTS();
}
|