Test a specific exception type is thrown AND the exception has the right properties

72,193

Solution 1

I mostly second Lilshieste's answer but would add that you also should verify that the wrong exception type is not thrown:

#include <stdexcept>
#include "gtest/gtest.h"

struct foo
{
    int bar(int i) {
        if (i > 100) {
            throw std::out_of_range("Out of range");
        }
        return i;
    }
};

TEST(foo_test,out_of_range)
{
    foo f;
    try {
        f.bar(111);
        FAIL() << "Expected std::out_of_range";
    }
    catch(std::out_of_range const & err) {
        EXPECT_EQ(err.what(),std::string("Out of range"));
    }
    catch(...) {
        FAIL() << "Expected std::out_of_range";
    }
}

int main(int argc, char **argv) {
  ::testing::InitGoogleTest(&argc, argv);
  return RUN_ALL_TESTS();
}

Solution 2

A colleague came up with the solution by just re-throwing the exception.

The knack: no need of extra FAIL() statements, just the two EXPECT... calls that test the bits you actually want: the exception as such and its value.

TEST(Exception, HasCertainMessage )
{
    // this tests _that_ the expected exception is thrown
    EXPECT_THROW({
        try
        {
            thisShallThrow();
        }
        catch( const MyException& e )
        {
            // and this tests that it has the correct message
            EXPECT_STREQ( "Cucumber overflow", e.what() );
            throw;
        }
    }, MyException );
}

Solution 3

Jeff Langr describes a good approach in his book, Modern C++ Programming with Test-Driven Development:

If your [testing] framework does not support a single-line declarative assert that ensures an exception is thrown, you can use the following structure in your test:

    TEST(ATweet, RequiresUserNameToStartWithAnAtSign) {
        string invalidUser("notStartingWith@");
        try {
            Tweet tweet("msg", invalidUser);
            FAIL();
        }
        catch(const InvalidUserException& expected) {}
    }

[...] You might also need to use the try-catch structure if you must verify any postconditions after the exception is thrown. For example, you may want to verify the text associated with the thrown exception object.

    TEST(ATweet, RequiresUserNameToStartWithAtSign) {
        string invalidUser("notStartingWith@");
        try {
            Tweet tweet("msg", invalidUser);
            FAIL();
        }
        catch(const InvalidUserException& expected) {
            ASSERT_STREQ("notStartingWith@", expected.what());
        }
    }

(p.95)

This is the approach I've used, and have seen in practice elsewhere.

Edit: As has been pointed out by @MikeKinghan, this doesn't quite match the functionality provided by EXPECT_THROW; the test doesn't fail if the wrong exception is thrown. An additional catch clause could be added to address this:

catch(...) {
    FAIL();
}

Solution 4

I had previously offered a macro to solve this in an older answer. However time has passed and a new feature was added to GTest, which allows for this without macros.

The feature is a set of matchers, e.g., Throws that can be used in combination with EXPECT_THAT(). However the documentation does not seem to have been updated, so the only information is hidden in this GitHub issue.


The feature is used like this:

EXPECT_THAT([]() { throw std::runtime_error("message"); },
    Throws<std::runtime_error>());

EXPECT_THAT([]() { throw std::runtime_error("message"); },
    ThrowsMessage<std::runtime_error>(HasSubstr("message")));

EXPECT_THAT([]() { throw std::runtime_error("message"); },
    ThrowsMessageHasSubstr<std::runtime_error>("message"));

EXPECT_THAT([]() { throw std::runtime_error("message"); },
    Throws<std::runtime_error>(Property(&std::runtime_error::what,
         HasSubstr("message"))));

Note that due to how EXPECT_THAT() works you need to put the throwing statement into something invokable without arguments. Hence the lambdas in the examples above.


Edit: This feature is included beginning with version 1.11.

Also note that this feature is not included in version 1.10, but it has been merged into master. Because GTest follows abseil's live at head policy there is are no new versions planned at the moment. Also they don't seem to follow abseil's policy to release specific versions for those of use who can't/won't live at head.

Solution 5

A new feature was added to GTest master on 2020-08-24 (post v1.10) which I explained in a separate answer. However I'll leave this answer because it still helps if the version you're using doesn't support the new feature.


As I need to do several of such tests I wrote a macro that basically includes Mike Kinghan's answer but "removes" all the boilerplate code:

#define ASSERT_THROW_KEEP_AS_E(statement, expected_exception) \
    std::exception_ptr _exceptionPtr; \
    try \
    { \
        (statement);\
        FAIL() << "Expected: " #statement " throws an exception of type " \
          #expected_exception ".\n  Actual: it throws nothing."; \
    } \
    catch (expected_exception const &) \
    { \
        _exceptionPtr = std::current_exception(); \
    } \
    catch (...) \
    { \
        FAIL() << "Expected: " #statement " throws an exception of type " \
          #expected_exception ".\n  Actual: it throws a different type."; \
    } \
    try \
    { \
        std::rethrow_exception(_exceptionPtr); \
    } \
    catch (expected_exception const & e)

Usage:

ASSERT_THROW_KEEP_AS_E(foo(), MyException)
{
    ASSERT_STREQ("Cucumber overflow", e.msg());
}

Caveats:

  • As the macro defines a variable in the current scope, so it can only be used once.
  • C++11 is needed for std::exception_ptr
Share:
72,193
Mr. Boy
Author by

Mr. Boy

SOreadytohelp

Updated on February 13, 2022

Comments

  • Mr. Boy
    Mr. Boy about 2 years

    I want to test that MyException is thrown in a certain case. EXPECT_THROW is good here. But I also want to check the exception has a specific state e.g e.msg() == "Cucumber overflow".

    How is this best implemented in GTest?