Framework overview
Google Test (also known as googletest) is a C++ unit testing framework developed by Google. Its first version was released in 2004 as Google's internal testing framework. Subsequently, Google Test became widely used in the open source community and became the C++ unit testing framework of choice in many projects and organizations.
Google Test provides a rich set of assertion functions and test macros that enable developers to write clean, readable, and maintainable unit tests. It supports functions such as test fixtures, parameterized tests, and test suites to meet various testing needs.
Google Test continues to be improved and updated over time, adding many new features and improvements. Its codebase is hosted on GitHub and is maintained and updated by the community.
In 2017, the latest version of Google Test is 1.8.1. However, the development of the Google Test framework did not stop there, and subsequent versions were developed and updated by a broad developer community to meet changing testing needs.
The success and popularity of Google Test is due to its ease of use, flexibility and scalability, and its admiration for high-quality code and unit testing. It has become a widely adopted unit testing framework in the C++ community, providing developers with powerful testing tools and practices.
The official website address of googletest: https://google.github.io/googletest/
Open source address of googletest: https://github.com/google/googletest
Sample example of googletest: https://github.com/google/googletest/tree/main/googletest/samples
bazel uses run mode
In bazel we want to use googletest, which is very simple.
There is an article on the official website that directly talks about how to use bazel to introduce googletest: https://google.github.io/googletest/quickstart-bazel.html
Introduce googletest in WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "com_google_googletest",
urls = ["https://github.com/google/googletest/archive/5ab508a01f9eb089207ee87fd547d290da39d015.zip"],
strip_prefix = "googletest-5ab508a01f9eb089207ee87fd547d290da39d015",
)
Write BUILD's cc_test
Suppose the target file is:
# https://docs.bazel.build/versions/master/be/c-cpp.html#cc_library
cc_library(
name = "sample1",
srcs = ["sample1.cc"],
hdrs = ["sample1.h"],
)
cc_test(
name = "sample1_unittest",
srcs = ["sample1_unittest.cc"],
deps = [
"@com_google_googletest//:gtest_main",
":sample1",
],
)
run bazel test
bazel test --test_output=all media_cpp_demo/cpp_unit_test:sample1_unittest
This will output similar information:
INFO: From Testing //media_cpp_demo/cpp_unit_test:sample1_unittest:
==================== Test output for //media_cpp_demo/cpp_unit_test:sample1_unittest:
Running main() from gmock_main.cc
[==========] Running 6 tests from 2 test suites.
[----------] Global test environment set-up.
[----------] 3 tests from FactorialTest
[ RUN ] FactorialTest.Negative
[ OK ] FactorialTest.Negative (0 ms)
[ RUN ] FactorialTest.Zero
[ OK ] FactorialTest.Zero (0 ms)
[ RUN ] FactorialTest.Positive
media_cpp_demo/cpp_unit_test/sample1_unittest.cc:25: Failure
Expected equality of these values:
3
Factorial(3)
Which is: 6
[ FAILED ] FactorialTest.Positive (0 ms)
[----------] 3 tests from FactorialTest (0 ms total)
[----------] 3 tests from IsPrimeTest
[ RUN ] IsPrimeTest.Negative
[ OK ] IsPrimeTest.Negative (0 ms)
[ RUN ] IsPrimeTest.Trivial
[ OK ] IsPrimeTest.Trivial (0 ms)
[ RUN ] IsPrimeTest.Positive
[ OK ] IsPrimeTest.Positive (0 ms)
[----------] 3 tests from IsPrimeTest (0 ms total)
[----------] Global test environment tear-down
[==========] 6 tests from 2 test suites ran. (0 ms total)
[ PASSED ] 5 tests.
[ FAILED ] 1 test, listed below:
[ FAILED ] FactorialTest.Positive
1 FAILED TEST
================================================================================
It clearly tells us which cases have run and which cases have succeeded/failed.
Concrete writing
The best reference for writing is the sample example on the git official website of googletest.
Sample example of googletest: https://github.com/google/googletest/tree/main/googletest/samples
assert statement
In Google Test, there are a variety of assertion functions that can be used to write test assertions, including EXPECT_*
the and ASSERT_*
series. These two families of assertion functions are very similar in usage, but differ slightly in their behavior when an assertion fails.
Examples of commonly used assertion functions are listed below:
EXPECT_EQ
ANDASSERT_EQ
: Verifies that two values are equal.EXPECT_NE
ANDASSERT_NE
: Validates that two values are not equal.EXPECT_TRUE
ANDASSERT_TRUE
: Verify that the condition is true.EXPECT_FALSE
ANDASSERT_FALSE
: Verify that the condition is false.EXPECT_LT
AndASSERT_LT
: Verifies that the first value is less than the second value.EXPECT_LE
ANDASSERT_LE
: Verifies that the first value is less than or equal to the second value.EXPECT_GT
ANDASSERT_GT
: Validates that the first value is greater than the second value.EXPECT_GE
ANDASSERT_GE
: Validates that the first value is greater than or equal to the second value.
These assert functions behave slightly differently on assertion failures:
EXPECT_*
Series: If the assertion fails, an error message will be printed, but the test function continues to execute.ASSERT_*
Series: If the assertion fails, an error message will be output and the execution of the current test function will be terminated.
Here is an example that demonstrates how to use these assertion functions:
#include <gtest/gtest.h>
TEST(MyTestSuite, ExampleTest) {
int x = 5;
int y = 10;
// 验证相等关系
EXPECT_EQ(x, 5);
ASSERT_NE(x, y);
// 验证条件
EXPECT_TRUE(x > 0);
ASSERT_FALSE(y < 0);
// 验证大小关系
EXPECT_LT(x, y);
ASSERT_GE(y, 10);
}
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
In the above example, we defined a test case MyTestSuite.ExampleTest
. In test cases, we have used different assertion functions to verify conditions and relations. Depending on the use of the assertion function, you can choose to use the EXPECT_*
or ASSERT_*
series to meet your testing needs.
When running a test, if an assertion fails, Google Test will output an error message indicating exactly where and under what conditions the assertion failed.
Depending on your testing needs and personal preference, you can choose to use the appropriate assertion function to write test assertions, and choose to use the EXPECT_*
or ASSERT_*
series according to the test situation.
simple test case
A simple test case would be to use the macroTest
You can refer to: https://github.com/google/googletest/blob/main/googletest/samples/sample1_unittest.cc
TEST(FactorialTest, Positive) {
EXPECT_EQ(1, Factorial(1));
EXPECT_EQ(2, Factorial(2));
EXPECT_EQ(6, Factorial(3));
EXPECT_EQ(40320, Factorial(8));
}
Here FactorialTest is the test fixture (TestSuite), and Positive is the test case (TestCase).
In the final output, the same TestSuite will be displayed together.
Multiple test cases share data
What if our multiple test cases have some initialization operations? Macros are needed here TEST_F
.
You can refer to: https://github.com/google/googletest/blob/main/googletest/samples/sample3_unittest.cc
TEST_F
Is a macro in the Google Test framework used to define test cases based on test fixtures (Test Fixture). Test fixtures provide settings and state that are shared between multiple test cases.
Using TEST_F
macros, you can define multiple test cases in a test fixture class and share member variables and functions in that class. Each test case will execute the setup (SetUp) function of the fixture before running, and execute the cleanup (TearDown) function of the fixture after running.
Here is a TEST_F
sample code for defining a test case using the macro:
#include <gtest/gtest.h>
// 测试夹具类
class MyTestFixture : public ::testing::Test {
protected:
void SetUp() override {
// 在每个测试用例之前执行的设置
}
void TearDown() override {
// 在每个测试用例之后执行的清理
}
// 夹具类中的成员变量和函数
int value;
};
// 使用 TEST_F 宏定义测试用例
TEST_F(MyTestFixture, TestCase1) {
// 使用夹具类中的成员变量和函数进行测试断言
value = 42;
EXPECT_EQ(value, 42);
}
TEST_F(MyTestFixture, TestCase2) {
// ...
}
// ...
int main(int argc, char* argv[]) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
In the above example, we created a MyTestFixture
test fixture class called , which inherits from ::testing::Test
. In the fixture class, we can define member variables and functions, and override SetUp
and TearDown
functions to set and clean up the shared state of the test.
Then, we use TEST_F
macros to define test cases. In each test case, we can use the member variables and functions in the fixture class and write test assertions.
Finally, we initialize the Google Test framework and run all test cases.
By using TEST_F
macros, we can more conveniently organize and manage test cases based on test fixtures to ensure that they share the same settings and states, and provide more flexible test scenarios.
In fact, it is not difficult to see that the simple test case TEST is actually a special case of TEST_F, and TEST is more like implicitly and automatically filling the TestSuite defined by TEST_F to the background.
Custom main function
Similarly, there is no main function in the test case we defined, but it is also implicitly and automatically filled by the framework. Its prototype is:
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
We can also write such a function in the unit test file.
When may you need to write the main function yourself?
If all your TestSuites have a common data initialization logic, it can only be written in this main function.
How to write good unit tests?
The official website https://google.github.io/googletest/primer.html briefly talks about how to write a good test case. In fact, well-written unit tests have nothing to do with language. The following guidelines apply to unit tests in all languages.
Well-written unit tests are key to ensuring code correctness and reliability. Here are some guidelines on how to write good unit tests:
Clarify test goals: Before writing unit tests, clarify what the goals of the tests are. Know the behavior, inputs, and expected outputs of the code unit under test, as well as possible boundary conditions and exceptions.
Single Responsibility Principle: Make sure that each unit test only focuses on one specific function or behavior. Break down test cases into independent and repeatable units to make it easier to locate and fix problems.
Cover various cases: When writing test cases, make sure to cover different combinations of inputs, boundary conditions, and exceptions. Tests should verify that the code behaves correctly in various situations.
Independence and repeatability: Each test case should be independent and repeatable. There should be no dependencies between test cases to avoid mutual influence between tests.
Names are clear and unambiguous: Give test cases and assertions clear and unambiguous names so that the purpose and expected results of the test can be accurately described. This makes it easier to understand what the test is for, and to locate problems when a test fails.
Use appropriate assertions: Choose appropriate assertion functions to verify the expected behavior of your code. Make sure assertions are clear and provide useful error messages when tests fail, so you can quickly pinpoint problems.
Auxiliary tools and frameworks: Use appropriate unit testing frameworks and auxiliary tools such as Google Test, Catch2, Mockito, etc. to simplify writing and managing tests. These tools can provide rich assertions and helper functions, as well as features such as test running and report generation.
Maintain test maintainability: as code evolves and changes, test cases are updated and maintained in a timely manner. Make sure the tests are kept in sync with the code and still be able to effectively verify the correctness of the code.
Consider edge cases and exception handling: Tests should cover various edge cases and exception handling to verify that the code behaves as expected in those cases. Including boundary values, unusual inputs, null values, etc.
Run the test suite on a regular basis: Make sure to run the entire test suite on a regular basis to catch potential issues early. Integration tests are run into a continuous integration (CI) system to automate test execution and obtain test results in a timely manner.
Well-written unit tests can provide high code coverage and reliability, helping you catch problems, prevent regressions, and provide confidence in code improvements. Following the principles above and testing aggressively are important practices to ensure code quality.
How can test coverage be generated
googletest itself does not provide direct test coverage functionality, but you can combine other tools to generate test coverage reports.
If you are using bazel, then this step is even easier.
Generate code coverage data
This bazel has a bazel coverage code that can generate coverage.
First, modify the BUILD file first, and increase the parameters corresponding to the generated coverage.
# https://docs.bazel.build/versions/master/be/c-cpp.html#cc_library
cc_library(
name = "sample1",
srcs = ["sample1.cc"],
hdrs = ["sample1.h"],
)
cc_test(
name = "sample1_unittest",
srcs = ["sample1_unittest.cc"],
copts = ["-fprofile-arcs", "-ftest-coverage"],
linkopts = ["-fprofile-arcs"],
deps = [
"@com_google_googletest//:gtest_main",
":sample1",
],
)
Second, run the commandbazel coverage media_cpp_demo/cpp_unit_test:sample1_unittest
The output shows the coverage data path:
INFO: Build completed successfully, 25 total actions
//media_cpp_demo/cpp_unit_test:sample1_unittest PASSED in 0.3s
/root/.cache/bazel/_bazel_root/aa4e8447fb143c448ba118077e918987/execroot/__main__/bazel-out/k8-fastbuild/testlogs/media_cpp_demo/cpp_unit_test/sample1_unittest/coverage.dat
The coverage.dat here is the coverage data we generated.
Generate code coverage html
This step needs to rely on a genhtml tool. Not to mention downloading and installing.
Directly on the command:
genhtml ./bazel-out/k8-fastbuild/testlogs/media_cpp_demo/cpp_unit_test/sample1_unittest/coverage.dat --output-directory ./cov
Generate html from coverage.dat, and put html in the ./cov directory.
![image-20230710172120373](../../../Library/Application Support/typora-user-images/image-20230710172120373.png)
Open it and you will see the following measurement report
You can click further to see which rows are covered.
Summarize
There is nothing special about CPP unit tests and unit tests in other languages. The concepts and methods are the same.
Compared with how to write unit tests, how to write unit tests is more difficult. That's another topic.
reference
Master Google C++ unit testing framework GoogleTest in one article