From 9ac4cd0f4958608961f9b5ca27b99fe696e1dd27 Mon Sep 17 00:00:00 2001 From: Vladimir Goncharov Date: Sat, 20 Jun 2020 11:41:25 +0300 Subject: [PATCH] Add matchers for testing exception properties This PR adds matchers that accept a callable and verify that when invoked, it throws an exception with the given type and properties. Fixes #952 --- googlemock/include/gmock/gmock-matchers.h | 134 ++++++++++++++++ googlemock/test/gmock-matchers_test.cc | 182 ++++++++++++++++++++++ 2 files changed, 316 insertions(+) diff --git a/googlemock/include/gmock/gmock-matchers.h b/googlemock/include/gmock/gmock-matchers.h index 0bc9b364..43456c25 100644 --- a/googlemock/include/gmock/gmock-matchers.h +++ b/googlemock/include/gmock/gmock-matchers.h @@ -4725,6 +4725,140 @@ PolymorphicMatcher > VariantWith( internal::variant_matcher::VariantMatcher(matcher)); } + +#if GTEST_HAS_EXCEPTIONS + +// Anything inside the 'internal' namespace IS INTERNAL IMPLEMENTATION +// and MUST NOT BE USED IN USER CODE!!! +namespace internal { + +template +class ExceptionMatcherImpl { + public: + ExceptionMatcherImpl(Matcher matcher) + : matcher_(std::move(matcher)) {} + + public: + void DescribeTo(::std::ostream* os) const { + *os << "throws an exception of type " << GetTypeName(); + if (matcher_.GetDescriber() != nullptr) { + *os << " which "; + matcher_.DescribeTo(os); + } + } + + void DescribeNegationTo(::std::ostream* os) const { + *os << "not ("; + DescribeTo(os); + *os << ")"; + } + + template + bool MatchAndExplain(T&& x, MatchResultListener* listener) const { + try { + (void)(std::forward(x)()); + } catch (const Err& err) { + *listener << "throws an exception of type " << GetTypeName(); + if (matcher_.GetDescriber() != nullptr) { + *listener << " "; + return matcher_.MatchAndExplain(err, listener); + } else { + return true; + } + } catch (const std::exception& err) { +#if GTEST_HAS_RTTI + *listener << "throws an exception of type " + << GetTypeName(typeid(err)) << " "; +#else + *listener << "throws an std::exception-derived error "; +#endif + *listener << "with description \"" << err.what() << "\""; + return false; + } catch (...) { + *listener << "throws an exception of some other type"; + return false; + } + *listener << "does not throw any exception"; + return false; + } + + private: + const Matcher matcher_; +}; + +} // namespace internal + +// Throws() +// Throws(exceptionMatcher) +// ThrowsMessage(messageMatcher) +// ThrowsMessageHasSubstr(message) +// +// This matcher accepts a callable and verifies that when invoked, it throws +// an exception with the given type and properties. +// +// Examples: +// +// EXPECT_THAT( +// []() { throw std::runtime_error("message"); }, +// Throws()); +// +// EXPECT_THAT( +// []() { throw std::runtime_error("message"); }, +// ThrowsMessage(HasSubstr("message"))); +// +// EXPECT_THAT( +// []() { throw std::runtime_error("message"); }, +// ThrowsMessageHasSubstr("message")); +// +// EXPECT_THAT( +// []() { throw std::runtime_error("message"); }, +// Throws + +template +PolymorphicMatcher> +Throws() { + return MakePolymorphicMatcher( + internal::ExceptionMatcherImpl{ + Matcher{}}); +} +template +PolymorphicMatcher> +Throws(const ExceptionMatcher& exceptionMatcher) { + // Using matcher cast allows users to pass a matcher of a more broad type. + // For example user may want to pass Matcher + // to Throws, or Matcher to Throws. + return MakePolymorphicMatcher( + internal::ExceptionMatcherImpl{ + SafeMatcherCast(exceptionMatcher)}); +} +template +PolymorphicMatcher> +ThrowsMessage(const MessageMatcher& messageMatcher) { + static_assert( + std::is_base_of::value, + "expected an std::exception-derived class"); + // We cast matcher to std::string so that we have string semantics instead of + // pointer semantics. With this cast, we can accept matchers that match types + // that're constructible from strings. Also, we can accept raw string + // literals, e.g. ThrowsMessage("message"). + return MakePolymorphicMatcher( + internal::ExceptionMatcherImpl{ + Property("description", &std::exception::what, + MatcherCast(messageMatcher))}); +} +template +PolymorphicMatcher> +ThrowsMessageHasSubstr(const internal::StringLike& message) { + static_assert( + std::is_base_of::value, + "expected an std::exception-derived class"); + return MakePolymorphicMatcher( + internal::ExceptionMatcherImpl{ + Property("description", &std::exception::what, HasSubstr(message))}); +} + +#endif // GTEST_HAS_EXCEPTIONS + // These macros allow using matchers to check values in Google Test // tests. ASSERT_THAT(value, matcher) and EXPECT_THAT(value, matcher) // succeed if and only if the value matches the matcher. If the assertion diff --git a/googlemock/test/gmock-matchers_test.cc b/googlemock/test/gmock-matchers_test.cc index 015bb09a..db2e0430 100644 --- a/googlemock/test/gmock-matchers_test.cc +++ b/googlemock/test/gmock-matchers_test.cc @@ -8117,6 +8117,188 @@ TEST(MatcherPMacroTest, WorksOnMoveOnlyType) { EXPECT_THAT(p, Not(UniquePointee(2))); } +#if GTEST_HAS_EXCEPTIONS + +TEST(ThrowsTest, Describe) { + Matcher matcher = Throws(); + std::stringstream ss; + matcher.DescribeTo(&ss); + auto explanation = ss.str(); + EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error")); +} + +TEST(ThrowsTest, Success) { + Matcher matcher = Throws(); + StringMatchResultListener listener; + EXPECT_TRUE( + matcher.MatchAndExplain( + []() { throw std::runtime_error("error message"); }, &listener)); + EXPECT_THAT(listener.str(), testing::HasSubstr("std::runtime_error")); +} + +TEST(ThrowsTest, FailWrongType) { + Matcher matcher = Throws(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { throw std::logic_error("error message"); }, &listener)); + EXPECT_THAT(listener.str(), testing::HasSubstr("std::logic_error")); + EXPECT_THAT(listener.str(), testing::HasSubstr("\"error message\"")); +} + +TEST(ThrowsTest, FailWrongTypeNonStd) { + Matcher matcher = Throws(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { throw 10; }, &listener)); + EXPECT_THAT( + listener.str(), + testing::HasSubstr("throws an exception of some other type")); +} + +TEST(ThrowsTest, FailNoThrow) { + Matcher matcher = Throws(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { (void)0; }, &listener)); + EXPECT_THAT( + listener.str(), + testing::HasSubstr("does not throw any exception")); +} + +class ThrowsPredicateTest: public TestWithParam> {}; + +TEST_P(ThrowsPredicateTest, Describe) { + Matcher matcher = GetParam(); + std::stringstream ss; + matcher.DescribeTo(&ss); + auto explanation = ss.str(); + EXPECT_THAT(explanation, testing::HasSubstr("std::runtime_error")); + EXPECT_THAT(explanation, testing::HasSubstr("error message")); +} + +TEST_P(ThrowsPredicateTest, Success) { + Matcher matcher = GetParam(); + StringMatchResultListener listener; + EXPECT_TRUE( + matcher.MatchAndExplain( + []() { throw std::runtime_error("error message"); }, &listener)); + EXPECT_THAT(listener.str(), testing::HasSubstr("std::runtime_error")); +} + +TEST_P(ThrowsPredicateTest, FailWrongType) { + Matcher matcher = GetParam(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { throw std::logic_error("error message"); }, &listener)); + EXPECT_THAT(listener.str(), testing::HasSubstr("std::logic_error")); + EXPECT_THAT(listener.str(), testing::HasSubstr("\"error message\"")); +} + +TEST_P(ThrowsPredicateTest, FailWrongTypeNonStd) { + Matcher matcher = GetParam(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { throw 10; }, &listener)); + EXPECT_THAT( + listener.str(), + testing::HasSubstr("throws an exception of some other type")); +} + +TEST_P(ThrowsPredicateTest, FailWrongMessage) { + Matcher matcher = GetParam(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { throw std::runtime_error("wrong message"); }, &listener)); + EXPECT_THAT(listener.str(), testing::HasSubstr("std::runtime_error")); + EXPECT_THAT(listener.str(), testing::HasSubstr("wrong message")); +} + +TEST_P(ThrowsPredicateTest, FailNoThrow) { + Matcher matcher = GetParam(); + StringMatchResultListener listener; + EXPECT_FALSE( + matcher.MatchAndExplain( + []() { (void)0; }, &listener)); + EXPECT_THAT( + listener.str(), + testing::HasSubstr("does not throw any exception")); +} + +INSTANTIATE_TEST_SUITE_P(AllMessagePredicates, ThrowsPredicateTest, + ::testing::Values( + static_cast>( + Throws( + Property(&std::exception::what, HasSubstr("error message")))), + static_cast>( + ThrowsMessage(HasSubstr("error message"))), + static_cast>( + ThrowsMessageHasSubstr("error message")))); + +// Tests that Throws(Matcher{}) compiles even when E2 != const E1&. +TEST(ThrowsPredicateCompilesTest, ExceptionMatcherAcceptsBroadType) { + { + Matcher inner = + Property(&std::exception::what, HasSubstr("error message")); + Matcher matcher = Throws(inner); + EXPECT_TRUE( + matcher.Matches([]() { throw std::runtime_error("error message"); })); + EXPECT_FALSE( + matcher.Matches([]() { throw std::runtime_error("wrong message"); })); + } + + { + Matcher inner = Eq(10); + Matcher matcher = Throws(inner); + EXPECT_TRUE( + matcher.Matches([]() { throw (uint32_t)10; })); + EXPECT_FALSE( + matcher.Matches([]() { throw (uint32_t)11; })); + } +} + +// Tests that ThrowsMessage("message") is equivalent +// to ThrowsMessage(Eq("message")). +TEST(ThrowsPredicateCompilesTest, MessageMatcherAcceptsNonMatcher) { + Matcher matcher = ThrowsMessage( + "error message"); + EXPECT_TRUE( + matcher.Matches( + []() { throw std::runtime_error("error message"); })); + EXPECT_FALSE( + matcher.Matches( + []() { throw std::runtime_error("wrong error message"); })); +} + +// Tests that ThrowsMessageHasSubstr accepts types that're +// explicitly-convertible to std::string. +TEST(ThrowsPredicateCompilesTest, StringLikeMessage) { + struct SomeCustomString { + std::string inner; + + // Note: explicit conversion. + explicit operator std::string() const { + return inner; + } +}; + + Matcher matcher = ThrowsMessageHasSubstr( + SomeCustomString{"error message"}); + EXPECT_TRUE( + matcher.Matches( + []() { throw std::runtime_error("error message"); })); + EXPECT_FALSE( + matcher.Matches( + []() { throw std::runtime_error("wrong message"); })); +} + +#endif // GTEST_HAS_EXCEPTIONS + } // namespace } // namespace gmock_matchers_test } // namespace testing