mirror of
https://github.com/LCTT/TranslateProject.git
synced 2024-12-26 21:30:55 +08:00
372 lines
12 KiB
Markdown
372 lines
12 KiB
Markdown
|
[#]: subject: "Perform unit tests using GoogleTest and CTest"
|
|||
|
[#]: via: "https://opensource.com/article/22/1/unit-testing-googletest-ctest"
|
|||
|
[#]: author: "Stephan Avenwedde https://opensource.com/users/hansic99"
|
|||
|
[#]: collector: "lujun9972"
|
|||
|
[#]: translator: " "
|
|||
|
[#]: reviewer: " "
|
|||
|
[#]: publisher: " "
|
|||
|
[#]: url: " "
|
|||
|
|
|||
|
Perform unit tests using GoogleTest and CTest
|
|||
|
======
|
|||
|
Using unit tests will likely improve your code's quality and do so
|
|||
|
without disturbing your workflow.
|
|||
|
![Team checklist and to dos][1]
|
|||
|
|
|||
|
This article is a follow-up to my last article [Set up a build system with CMake and VSCodium][2].
|
|||
|
|
|||
|
In the last article, I showed how to configure a build system based on [VSCodium][3] and [CMake][4]. This article refines this setup by integrating meaningful unit tests using [GoogleTest][5] and [CTest][6].
|
|||
|
|
|||
|
If not already done, clone the [repository][7], open it in VSCodium and checkout the tag _devops_2_ by clicking on the _main_-branch symbol (red marker) and choosing the branch (yellow marker):
|
|||
|
|
|||
|
![VSCodium tag][8]
|
|||
|
|
|||
|
Stephan Avenwedde (CC BY-SA 4.0)
|
|||
|
|
|||
|
Alternatively, open the command line and type:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
`$ git checkout tags/devops_2`
|
|||
|
```
|
|||
|
|
|||
|
### GoogleTest
|
|||
|
|
|||
|
GoogleTest is a platform-independent, open source C++ testing framework. Even though GoogleTest is not meant to be exclusively for unit tests, I will use it to define unit tests for the _Generator_ library. In general, a unit test should verify the behavior of a single, logical unit. The _Generator_ library is one unit, so I'll write some meaningful tests to ensure proper function.
|
|||
|
|
|||
|
Using GoogleTest, the test cases are defined by assertions macros. Processing an assertion generates one of the following results:
|
|||
|
|
|||
|
* _Success_: Test passed.
|
|||
|
* _Nonfatal failure_: Test failed, but the test function will continue.
|
|||
|
* _Fatal failure_: Test failed, and the test function will be aborted.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
The assertions macros follow this scheme to distinguish a fatal from a nonfatal failure:
|
|||
|
|
|||
|
* `ASSERT_*` fatal failure, function is aborted.
|
|||
|
* `EXPECT_*` nonfatal failure, function is not aborted.
|
|||
|
|
|||
|
|
|||
|
|
|||
|
Google recommends using `EXPECT_*` macros as they allow the test to continue when the tests define multiple assertions. An assertion macro takes two arguments: The first argument is the name of the test group (a freely selectable string), and the second argument is the name of the test itself. The _Generator_ library just defines the function _generate(...)_, therefore the tests in this article belong to the same group: _GeneratorTest_.
|
|||
|
|
|||
|
The following unit tests for the _generate(...)_ function can be found in [GeneratorTest.cpp][9].
|
|||
|
|
|||
|
#### Reference check
|
|||
|
|
|||
|
The [generate(...)][10] function takes a reference to a [std::stringstream][11] as an argument and returns the same reference. So the first test is to check if the passed reference is the same reference which the function returns.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
TEST(GeneratorTest, ReferenceCheck){
|
|||
|
const int NumberOfElements = 10;
|
|||
|
std::stringstream buffer;
|
|||
|
EXPECT_EQ(
|
|||
|
std::addressof(buffer),
|
|||
|
std::addressof(Generator::generate(buffer, NumberOfElements))
|
|||
|
);
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
Here I use [std::addressof][12] to check if the address of the returned object refers to the same object I provided as input.
|
|||
|
|
|||
|
#### Number of elements
|
|||
|
|
|||
|
This test checks if the number of elements in the stringstream reference matches the number given as an argument.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
TEST(GeneratorTest, NumberOfElements){
|
|||
|
const int NumberOfElements = 50;
|
|||
|
int nCalcNoElements = 0;
|
|||
|
|
|||
|
std::stringstream buffer;
|
|||
|
|
|||
|
Generator::generate(buffer, NumberOfElements);
|
|||
|
std::string s_no;
|
|||
|
|
|||
|
while(std::getline(buffer, s_no, ' ')) {
|
|||
|
nCalcNoElements++;
|
|||
|
}
|
|||
|
|
|||
|
EXPECT_EQ(nCalcNoElements, NumberOfElements);
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
#### Shuffle
|
|||
|
|
|||
|
This test checks the proper working of the random engine. If I invoke the _generate_ function two times in a row, I expect not to get the same result.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
TEST(GeneratorTest, Shuffle){
|
|||
|
|
|||
|
const int NumberOfElements = 50;
|
|||
|
|
|||
|
std::stringstream buffer_A;
|
|||
|
std::stringstream buffer_B;
|
|||
|
|
|||
|
Generator::generate(buffer_A, NumberOfElements);
|
|||
|
Generator::generate(buffer_B, NumberOfElements);
|
|||
|
|
|||
|
EXPECT_NE(buffer_A.str(), buffer_B.str());
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
#### Checksum
|
|||
|
|
|||
|
This is the largest test. It checks whether the sum of the digits of a numerical series from 1 to _n_ is the same as the sum of the shuffled output series. I expect that the sum matches as the _generate(...)_ function should simply create a shuffled variant of such a series.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
TEST(GeneratorTest, CheckSum){
|
|||
|
|
|||
|
const int NumberOfElements = 50;
|
|||
|
int nChecksum_in = 0;
|
|||
|
int nChecksum_out = 0;
|
|||
|
|
|||
|
std::vector<int> vNumbersRef(NumberOfElements); // Input vector
|
|||
|
std::iota(vNumbersRef.begin(), vNumbersRef.end(), 1); // Populate vector
|
|||
|
|
|||
|
// Calculate reference checksum
|
|||
|
for(const int n : vNumbersRef){
|
|||
|
nChecksum_in += n;
|
|||
|
}
|
|||
|
|
|||
|
std::stringstream buffer;
|
|||
|
Generator::generate(buffer, NumberOfElements);
|
|||
|
|
|||
|
std::vector<int> vNumbersGen; // Output vector
|
|||
|
std::string s_no;
|
|||
|
|
|||
|
// Read the buffer back back to the output vector
|
|||
|
while(std::getline(buffer, s_no, ' ')) {
|
|||
|
vNumbersGen.push_back(std::stoi(s_no));
|
|||
|
}
|
|||
|
|
|||
|
// Calculate output checksum
|
|||
|
for(const int n : vNumbersGen){
|
|||
|
nChecksum_out += n;
|
|||
|
}
|
|||
|
|
|||
|
EXPECT_EQ(nChecksum_in, nChecksum_out);
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
The above tests can also be debugged like an ordinary C++ application.
|
|||
|
|
|||
|
### CTest
|
|||
|
|
|||
|
In addition to the in-code unit test, the [CTest][6] utility lets me define tests that can be performed on executables. In a nutshell, I call the executable with certain arguments and match the output with [regular expressions][13]. This lets me simply check how the executable behaves with incorrect command-line arguments. The tests are defined in the top level [CMakeLists.txt][14]. Here is a closer look at three test cases:
|
|||
|
|
|||
|
#### Regular usage
|
|||
|
|
|||
|
If a positive integer is provided as a command-line argument, I expect the executable to produce a series of numbers separated by whitespace:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
add_test(NAME RegularUsage COMMAND Producer 10)
|
|||
|
set_tests_properties(RegularUsage
|
|||
|
PROPERTIES PASS_REGULAR_EXPRESSION "^[0-9 ]+"
|
|||
|
)
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
#### No argument
|
|||
|
|
|||
|
If no argument is provided, the program should exit immediately and display the reason why:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
add_test(NAME NoArg COMMAND Producer)
|
|||
|
set_tests_properties(NoArg
|
|||
|
PROPERTIES PASS_REGULAR_EXPRESSION "^Enter the number of elements as argument"
|
|||
|
)
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
#### Wrong argument
|
|||
|
|
|||
|
Providing an argument that cannot be converted into an integer should also cause an immediate exit with an error message. This test invokes the _Producer_ executable with the command line parameter*"ABC"*:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
add_test(NAME WrongArg COMMAND Producer ABC)
|
|||
|
set_tests_properties(WrongArg
|
|||
|
PROPERTIES PASS_REGULAR_EXPRESSION "^Error: Cannot parse"
|
|||
|
)
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
#### Testing the tests
|
|||
|
|
|||
|
To run a single test and see how it is processed, invoke `ctest` from the command line providing the following arguments:
|
|||
|
|
|||
|
* Run single tst: `-R <test-name>`
|
|||
|
* Enable verbose output: `-VV`
|
|||
|
|
|||
|
|
|||
|
|
|||
|
Here is the command `ctest -R Usage -VV:`
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
$ ctest -R Usage -VV
|
|||
|
UpdatecTest Configuration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
|
|||
|
UpdateCTestConfiguration from :/home/stephan/Documents/cpp_testing sample/build/DartConfiguration.tcl
|
|||
|
Test project /home/stephan/Documents/cpp_testing sample/build
|
|||
|
Constructing a list of tests
|
|||
|
Done constructing a list of tests
|
|||
|
Updating test list for fixtures
|
|||
|
Added 0 tests to meet fixture requirements
|
|||
|
Checking test dependency graph...
|
|||
|
Checking test dependency graph end
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
In this code block, I invoked a test named _Usage_.
|
|||
|
|
|||
|
This ran the executable with no command-line arguments:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
test 3
|
|||
|
Start 3: Usage
|
|||
|
3: Test command: /home/stephan/Documents/cpp testing sample/build/Producer
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
The test failed because the output didn't match the regular expression `[^[0-9]+]`.
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
3: Enter the number of elements as argument
|
|||
|
1/1 test #3. Usage ................
|
|||
|
|
|||
|
Failed Required regular expression not found.
|
|||
|
Regex=[^[0-9]+]
|
|||
|
|
|||
|
0.00 sec round.
|
|||
|
|
|||
|
0% tests passed, 1 tests failed out of 1
|
|||
|
Total Test time (real) =
|
|||
|
0.00 sec
|
|||
|
The following tests FAILED:
|
|||
|
3 - Usage (Failed)
|
|||
|
Errors while running CTest
|
|||
|
$
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
To run all tests (including the one defined with GoogleTest), navigate to the _build_ directory and run `ctest`:
|
|||
|
|
|||
|
![CTest run][15]
|
|||
|
|
|||
|
Stephan Avenwedde (CC BY-SA 4.0)
|
|||
|
|
|||
|
Inside VSCodium, click on the area marked yellow in the info bar to invoke CTest. If all tests pass, the following output is displayed:
|
|||
|
|
|||
|
![VSCodium][16]
|
|||
|
|
|||
|
Stephan Avenwedde (CC BY-SA 4.0)
|
|||
|
|
|||
|
### Automate testing with Git Hooks
|
|||
|
|
|||
|
By now, running the tests is an additional step for the developer. The developer could also commit and push code that doesn't pass the tests. Thanks to [Git Hooks][17], I can implement a mechanism that automatically runs the tests and prevents the developer from accidentally committing faulty code.
|
|||
|
|
|||
|
Navigate to `.git/hooks`, create an empty file named _pre-commit_, and copy and paste the following code:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
|
|||
|
#!/usr/bin/sh
|
|||
|
|
|||
|
(cd build; ctest --output-on-failure -j6)
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
After it, make this file executable:
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
`$ chmod +x pre-commit`
|
|||
|
```
|
|||
|
|
|||
|
This script invokes CTest when trying to perform a commit. If a test fails, like in the screenshot below, the commit is aborted:
|
|||
|
|
|||
|
![Commit failed][18]
|
|||
|
|
|||
|
Stephan Avenwedde (CC BY-SA 4.0)
|
|||
|
|
|||
|
If the tests succeed, the commit is processed, and the output looks like this:
|
|||
|
|
|||
|
![Commit succeeded][19]
|
|||
|
|
|||
|
Stephan Avenwedde (CC BY-SA 4.0)
|
|||
|
|
|||
|
The described mechanism is only a soft barrier: A developer could still commit faulty code using `git commit --no-verify`. I can ensure that only working code is pushed by configuring a build server. This topic will be part of a separate article.
|
|||
|
|
|||
|
### Summary
|
|||
|
|
|||
|
The techniques mentioned in this article are easy to implement and help you quickly find bugs in your code. Using unit tests will likely improve your code's quality and, as I have shown, do so without disturbing your workflow. The GoogleTest framework provides features for every conceivable scenario; I only used a subset of its functionality. At this point, I also want to mention the [GoogleTest Primer][20], which gives you an overview of the ideas, opportunities, and features of the framework.
|
|||
|
|
|||
|
--------------------------------------------------------------------------------
|
|||
|
|
|||
|
via: https://opensource.com/article/22/1/unit-testing-googletest-ctest
|
|||
|
|
|||
|
作者:[Stephan Avenwedde][a]
|
|||
|
选题:[lujun9972][b]
|
|||
|
译者:[译者ID](https://github.com/译者ID)
|
|||
|
校对:[校对者ID](https://github.com/校对者ID)
|
|||
|
|
|||
|
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
|||
|
|
|||
|
[a]: https://opensource.com/users/hansic99
|
|||
|
[b]: https://github.com/lujun9972
|
|||
|
[1]: https://opensource.com/sites/default/files/styles/image-full-size/public/lead-images/todo_checklist_team_metrics_report.png?itok=oB5uQbzf (Team checklist and to dos)
|
|||
|
[2]: https://opensource.com/article/22/1/devops-cmake
|
|||
|
[3]: https://vscodium.com/
|
|||
|
[4]: https://cmake.org/
|
|||
|
[5]: https://github.com/google/googletest
|
|||
|
[6]: https://cmake.org/cmake/help/latest/manual/ctest.1.html
|
|||
|
[7]: https://github.com/hANSIc99/cpp_testing_sample
|
|||
|
[8]: https://opensource.com/sites/default/files/cpp_unit_test_vscodium_tag.png (VSCodium tag)
|
|||
|
[9]: https://github.com/hANSIc99/cpp_testing_sample/blob/main/Generator/GeneratorTest.cpp
|
|||
|
[10]: https://github.com/hANSIc99/cpp_testing_sample/blob/main/Generator/Generator.cpp
|
|||
|
[11]: https://en.cppreference.com/w/cpp/io/basic_stringstream
|
|||
|
[12]: https://en.cppreference.com/w/cpp/memory/addressof
|
|||
|
[13]: https://en.wikipedia.org/wiki/Regular_expression
|
|||
|
[14]: https://github.com/hANSIc99/cpp_testing_sample/blob/main/CMakeLists.txt
|
|||
|
[15]: https://opensource.com/sites/default/files/cpp_unit_test_ctest_run.png (CTest run)
|
|||
|
[16]: https://opensource.com/sites/default/files/cpp_unit_test_ctest_vscodium.png (VSCodium)
|
|||
|
[17]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks
|
|||
|
[18]: https://opensource.com/sites/default/files/cpp_unit_test_git_hook_commit_failed.png (Commit failed)
|
|||
|
[19]: https://opensource.com/sites/default/files/cpp_unit_test_git_hook_commit_succeeded.png (Commit succeeded)
|
|||
|
[20]: https://google.github.io/googletest/primer.html
|