Science and technology

Carry out unit checks utilizing GoogleTest and CTest

This article is a follow-up to my final article Set up a build system with CMake and VSCodium.

In the final article, I confirmed methods to configure a construct system primarily based on VSCodium and CMake. This article refines this setup by integrating significant unit checks utilizing GoogleTest and CTest.

If not already carried out, clone the repository, open it in VSCodium and checkout the tag devops_2 by clicking on the important-branch image (pink marker) and selecting the department (yellow marker):

Alternatively, open the command line and sort:

$ git checkout tags/devops_2

GoogleTest

GoogleTest is a platform-independent, open supply C++ testing framework. Even although GoogleTest isn’t meant to be solely for unit checks, I’ll use it to outline unit checks for the Generator library. In normal, a unit check ought to confirm the habits of a single, logical unit. The Generator library is one unit, so I’ll write some significant checks to make sure correct perform.

Using GoogleTest, the check circumstances are outlined by assertions macros. Processing an assertion generates one of many following outcomes:

  • Success: Test handed.
  • Nonfatal failure: Test failed, however the check perform will proceed.
  • Fatal failure: Test failed, and the check perform will probably be aborted.

The assertions macros comply with this scheme to tell apart a deadly from a nonfatal failure:

  • ASSERT_* deadly failure, perform is aborted.
  • EXPECT_* nonfatal failure, perform isn’t aborted.

Google recommends utilizing EXPECT_* macros as they permit the check to proceed when the checks outline a number of assertions. An assertion macro takes two arguments: The first argument is the title of the check group (a freely selectable string), and the second argument is the title of the check itself. The Generator library simply defines the perform generate(…), due to this fact the checks on this article belong to the identical group: GeneratorTest.

The following unit checks for the generate(…) perform could be present in GeneratorTest.cpp.

Reference verify

The generate(…) perform takes a reference to a std::stringstream as an argument and returns the identical reference. So the primary check is to verify if the handed reference is identical reference which the perform returns.

TEST(GeneratorTest, ReferenceExamine){
    const int NumberOfElements = 10;
    std::stringstream buffer;
    EXPECT_EQ(
        std::addressof(buffer),
        std::addressof(Generator::generate(buffer, NumberOfElements))
    );
}

Here I take advantage of std::addressof to verify if the handle of the returned object refers back to the similar object I offered as enter.

Number of parts

This check checks if the variety of parts within the stringstream reference matches the quantity given as an argument.

TEST(GeneratorTest, NumberOfElements){
    const int NumberOfElements = 50;
    int nCalcNoParts = 0;

    std::stringstream buffer;

    Generator::generate(buffer, NumberOfElements);
    std::string s_no;

    whereas(std::getline(buffer, s_no, ' ')) {
        nCalcNoParts++;
    }

    EXPECT_EQ(nCalcNoParts, NumberOfElements);
}

Shuffle

This check checks the correct working of the random engine. If I invoke the generate perform two instances in a row, I anticipate to not get the identical end 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 biggest check. It checks whether or not the sum of the digits of a numerical collection from 1 to n is identical because the sum of the shuffled output collection. I anticipate that the sum matches because the generate(…) perform ought to merely create a shuffled variant of such a collection.

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.start(), vNumbersRef.finish(), 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 again again to the output vector
    whereas(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 checks can be debugged like an bizarre C++ software.

CTest

In addition to the in-code unit check, the CTest utility lets me outline checks that may be carried out on executables. In a nutshell, I name the executable with sure arguments and match the output with regular expressions. This lets me merely verify how the executable behaves with incorrect command-line arguments. The checks are outlined within the prime degree CMakeLists.txt. Here is a better take a look at three check circumstances:

Regular utilization

If a constructive integer is offered as a command-line argument, I anticipate the executable to supply a collection 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 offered, this system ought to exit instantly and show the explanation 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 can not be transformed into an integer also needs to trigger a direct exit with an error message. This check 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 checks

To run a single check and see how it’s processed, invoke ctest from the command line offering the next 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 :/dwelling/stephan/Documents/cpp_testing pattern/construct/DartConfiguration.tcl
UpdateCTestConfiguration from :/dwelling/stephan/Documents/cpp_testing pattern/construct/DartConfiguration.tcl
Test mission /dwelling/stephan/Documents/cpp_testing pattern/construct
Constructing an inventory of checks
Done establishing an inventory of checks
Updating check listing for fixtures
Added 0 checks to satisfy fixture necessities
Checking check dependency graph...
Checking check dependency graph finish

In this code block, I invoked a check named Usage.

This ran the executable with no command-line arguments:

check 3
    Start 3: Usage
3: Test command: /dwelling/stephan/Documents/cpp testing pattern/construct/Producer

The check failed as a result of the output did not match the common expression [^[0-9]+].

3: Enter the variety of parts as argument
1/1 check #3. Usage ................

Failed Required common expression not discovered.
Regex=[^[0-9]+]

0.00 sec spherical.

0% checks handed, 1 checks failed out of 1
Total Test time (actual) =
0.00 sec
The following checks FAILED:
3 - Usage (Failed)
Errors whereas operating CTest
$

To run all checks (together with the one outlined with GoogleTest), navigate to the construct listing and run ctest:

Inside VSCodium, click on on the realm marked yellow within the information bar to invoke CTest. If all checks cross, the next output is displayed:

Automate testing with Git Hooks

By now, operating the checks is an extra step for the developer. The developer might additionally commit and push code that does not cross the checks. Thanks to Git Hooks, I can implement a mechanism that routinely runs the checks and prevents the developer from by accident committing defective code.

Navigate to .git/hooks, create an empty file named pre-commit, and replica and paste the next code:

#!/usr/bin/sh

(cd construct; ctest --output-on-failure -j6)

After it, make this file executable:

$ chmod +x pre-commit

This script invokes CTest when making an attempt to carry out a commit. If a check fails, like within the screenshot under, the commit is aborted:

If the checks succeed, the commit is processed, and the output appears to be like like this:

The described mechanism is simply a mushy barrier: A developer might nonetheless commit defective code utilizing git commit --no-verify. I can make sure that solely working code is pushed by configuring a construct server. This subject will probably be a part of a separate article.

Summary

The methods talked about on this article are straightforward to implement and enable you rapidly discover bugs in your code. Using unit checks will seemingly enhance your code’s high quality and, as I’ve proven, accomplish that with out disturbing your workflow. The GoogleTest framework offers options for each conceivable situation; I solely used a subset of its performance. At this level, I additionally need to point out the GoogleTest Primer, which provides you an outline of the concepts, alternatives, and options of the framework.

Most Popular

To Top