Search code examples
c++catch2

Different catch2 checks on different inputs


I'm trying to test some code that requires a little bit of setup to use, and I'd like to avoid repeating the setup steps. The setup steps have some dependency on the input, but the results could be significantly different depending on what exactly the inputs are. Is there a good way to set up a catch2 test case for this?

By way of example, let's consider a very basic object we might want to test:

#include <cmath>
struct SquareRoot {
  double n;
  double root() { return std::sqrt(n); }
};

If we wanted to write some tests for this, "positive" and "negative" are obvious ones to check ("zero" would be a third). For both of these, we need to create an object, that depends on the input, and then call the function, but the actual checks are very different.

#include <catch2/catch.hpp>
TEST_CASE("SquareRoot") {
  SECTION("positive") {
    double n = 4.0;
    SquareRoot sqrt{n};          // <---
    double result = sqrt.root(); // <---
    REQUIRE(result == 2.0);
  }

  SECTION("negative") {
    double n = -4.0;
    SquareRoot sqrt{n};          // <---
    double result = sqrt.root(); // <---
    REQUIRE(std::isnan(result));
  }
}

I could imagine GENERATE()ing the input, but then you need some way to decide what checks to actually run afterwards.

TEST_CASE("SquareRoot") {
  double n = GENERATE(4.0, -4.0);
  SquareRoot sqrt{n};
  double result = sqrt.root();
  // Which REQUIRE do I run here???
}

In the real system the object system is more complex. If the setup is more than a couple lines I've had some success breaking it out into a separate function, but it needs to be repeated in every test. Similarly, there's often more than one check on the result (for example, we might check errno in the negative case here).


Solution

  • I tend to write small helper functions (or lambdas) even for simple cases. For your example, this could be as short as:

    #include <catch2/catch.hpp>
    
    auto rootTest(double x)
    {
        return SquareRoot{x}.root();
    }
    
    TEST_CASE("SquareRoot") {
      SECTION("positive") {
        CHECK(rootTest(4.0) == 2.0);
        REQUIRE(errno == 0);
      }
    
      SECTION("negative") {
        CHECK(std::isnan(rootTest(-4.0)));
        REQUIRE(errno);
      }
    }
    

    In more complex cases, you could additionally define a POD struct to let your test function return multiple named values, which helps to keep the test conditions easily readable. You can also use Catch2 statements inside the function, in case you do have tests that apply regardless of input.