Read-only mirrors. Development, issues and pull requests are on GitHub.

summaryrefslogtreecommitdiffhomepage
path: root/tests
diff options
context:
space:
mode:
authorLeitwolf112026-11-01 19:04:52 +0300
committerLeitwolf112026-11-01 19:04:52 +0300
commitc51d15273e87267215e5877edc268e7db354187b (patch)
treebc314b97cbb793ea136ceabecc3d565240ae7cf2 /tests
parent26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff)
downloadcatch3-c51d15273e87267215e5877edc268e7db354187b.tar.gz
Add mocks that report through the assertion path
Step 3 of the roadmap. MockFunction checks the calls of one function against its expectations; CATCH3_MOCK_METHOD declares one for a virtual member, and CATCH3_MOCK_FUNCTION and ScopedSeam serve free functions behind a seam. Expectations take values, Catch matchers, Any or Where; count calls with Times, Between, AtLeast, AtMost, AnyTimes and Never; answer with Returns, Does or Throws; and order calls across mocks with Sequence. A failure goes through AssertionHandler at the line of the expectation, so every report of the runner shows it. Met expectations count as passed assertions. A test case that is already being left does not get unmet expectations on top of its own failure. Composed matchers refer to temporaries, so they are rejected at compile time rather than left dangling. A call that fails returns a value-initialized result, or leaves the test case when no such result exists. The failure paths are tested through the runner fixture, reading the messages from its JSON report.
Diffstat (limited to 'tests')
-rw-r--r--tests/CMakeLists.txt1
-rw-r--r--tests/Progmasoft/BUILD.bazel1
-rw-r--r--tests/Progmasoft/Mock.tests.cpp310
-rw-r--r--tests/Progmasoft/RunnerFixture.cpp67
-rw-r--r--tests/meson.build1
5 files changed, 380 insertions, 0 deletions
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 23185534..40ed1358 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -155,6 +155,7 @@ target_link_libraries(SelfTest PRIVATE Catch3WithMain)
# Exercise the Progmasoft layer independently of the upstream Catch2 SelfTest.
add_executable(ProgmasoftSelfTest
Progmasoft/Generators.tests.cpp
+ Progmasoft/Mock.tests.cpp
Progmasoft/Property.tests.cpp
Progmasoft/ForAll.tests.cpp
Progmasoft/Results.tests.cpp
diff --git a/tests/Progmasoft/BUILD.bazel b/tests/Progmasoft/BUILD.bazel
index 0571d862..871baaa8 100644
--- a/tests/Progmasoft/BUILD.bazel
+++ b/tests/Progmasoft/BUILD.bazel
@@ -9,6 +9,7 @@ cc_test(
srcs = [
"ForAll.tests.cpp",
"Generators.tests.cpp",
+ "Mock.tests.cpp",
"Property.tests.cpp",
"Results.tests.cpp",
"Runner.tests.cpp",
diff --git a/tests/Progmasoft/Mock.tests.cpp b/tests/Progmasoft/Mock.tests.cpp
new file mode 100644
index 00000000..5c8272c7
--- /dev/null
+++ b/tests/Progmasoft/Mock.tests.cpp
@@ -0,0 +1,310 @@
+// Copyright (c) 2026 Progmasoft.
+// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0
+
+#include <Progmasoft/Catch3/Matchers/Contains.hpp>
+#include <Progmasoft/Catch3/Matchers/String.hpp>
+#include <Progmasoft/Catch3/Mock.hpp>
+#include <Progmasoft/Catch3/TestMacros.hpp>
+#include <functional>
+#include <map>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include "RunnerFixtureClient.hpp"
+
+namespace
+{
+ using Catch::Matchers::ContainsSubstring;
+ using Catch::Matchers::StartsWith;
+ using FixtureClient::ExitCode;
+ using FixtureClient::RunFixture;
+ using FixtureClient::Scratch;
+ using Progmasoft::Catch3::Any;
+ using Progmasoft::Catch3::MockFunction;
+ using Progmasoft::Catch3::ScopedSeam;
+ using Progmasoft::Catch3::Sequence;
+ using Progmasoft::Catch3::Where;
+
+ using Scores = std::map<std::string, int>;
+
+ class Store
+ {
+ public:
+ virtual ~Store() = default;
+ virtual void
+ Open() = 0;
+ virtual int
+ Get(const std::string &key) const = 0;
+ virtual void
+ Put(const std::string &key, int value) = 0;
+ virtual void
+ Put(const Scores &scores) = 0;
+ virtual int
+ Sum(int a, int b, int c, int d, int e, int f, int g, int h) = 0;
+ virtual std::string &
+ Name() = 0;
+ virtual std::unique_ptr<int>
+ Take() = 0;
+ };
+
+ class MockStore final: public Store
+ {
+ public:
+ CATCH3_MOCK_METHOD(void, Open, (), override);
+ CATCH3_MOCK_METHOD(int, Get, (const std::string &), const override);
+ CATCH3_MOCK_OVERLOAD(void,
+ Put,
+ PutOne,
+ (const std::string &, int),
+ override);
+ CATCH3_MOCK_OVERLOAD(void, Put, PutAll, (const Scores &), override);
+ CATCH3_MOCK_METHOD(int,
+ Sum,
+ (int, int, int, int, int, int, int, int),
+ override);
+ CATCH3_MOCK_METHOD(std::string &, Name, (), override);
+ CATCH3_MOCK_METHOD(std::unique_ptr<int>, Take, (), override);
+ };
+
+ /// Production code with a seam for a free function.
+ std::function<long()> clockSeam = [] {
+ return 0L;
+ };
+
+ long
+ SecondsSince(long start)
+ {
+ return clockSeam() - start;
+ }
+
+ long (*pointerSeam)() = nullptr;
+} // namespace
+
+TEST_CASE("A mock method answers as its expectation says", "[progmasoft][mock]")
+{
+ MockStore store;
+ CATCH3_EXPECT_CALL(store.GetMock)("a").Returns(1);
+ CATCH3_EXPECT_CALL(store.GetMock)(StartsWith("b")).Times(2).Returns(2);
+ CATCH3_EXPECT_CALL(store.GetMock)(Any).Never();
+ CATCH3_EXPECT_CALL(store.OpenMock)();
+ CATCH3_EXPECT_CALL(store.PutOne)("a", 1);
+ CATCH3_EXPECT_CALL(store.PutAll)();
+
+ const Store &reading = store;
+ Store &writing = store;
+ writing.Open();
+ CHECK(reading.Get("a") == 1);
+ CHECK(reading.Get("bb") == 2);
+ CHECK(reading.Get("bc") == 2);
+ writing.Put("a", 1);
+ writing.Put(Scores{ { "a", 1 } });
+}
+
+TEST_CASE("Every parameter reaches an action", "[progmasoft][mock]")
+{
+ MockStore store;
+ CATCH3_EXPECT_CALL(store.SumMock)
+ (1,
+ Any,
+ 3,
+ Any,
+ 5,
+ Any,
+ 7,
+ Where(
+ [](int v) {
+ return v > 7;
+ },
+ "above 7"))
+ .Does([](int a, int b, int c, int d, int e, int f, int g, int h) {
+ return a + b + c + d + e + f + g + h;
+ });
+ CHECK(store.Sum(1, 2, 3, 4, 5, 6, 7, 8) == 36);
+}
+
+TEST_CASE("Calls are counted against limits", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, void());
+ CATCH3_MOCK_FUNCTION(tock, int(int));
+ CATCH3_EXPECT_CALL(tick)().AtLeast(2);
+ CATCH3_EXPECT_CALL(tock)(1).AtMost(1).Returns(10);
+ CATCH3_EXPECT_CALL(tock)(1).Between(1, 2).Returns(20);
+ CATCH3_EXPECT_CALL(tock)(2).AnyTimes();
+
+ tick();
+ tick();
+ tick();
+ CHECK(tock(1) == 10);
+ CHECK(tock(1) == 20);
+ CHECK(tock(1) == 20);
+ CHECK(tock(2) == 0);
+}
+
+TEST_CASE("Verify ends the expectations so far", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, int());
+ CATCH3_EXPECT_CALL(tick)().Returns(1);
+ CHECK(tick() == 1);
+ tick.Verify();
+ CATCH3_EXPECT_CALL(tick)().Returns(2);
+ CHECK(tick() == 2);
+}
+
+TEST_CASE("A sequence orders calls across mocks", "[progmasoft][mock]")
+{
+ Sequence order;
+ MockStore first;
+ MockStore second;
+ CATCH3_EXPECT_CALL(first.OpenMock)().InSequence(order);
+ CATCH3_EXPECT_CALL(second.GetMock)(Any).Times(2).InSequence(order);
+ CATCH3_EXPECT_CALL(first.GetMock)(Any).AtLeast(0).InSequence(order);
+ CATCH3_EXPECT_CALL(first.PutOne)(Any, Any).InSequence(order);
+
+ first.Open();
+ second.Get("a");
+ second.Get("b");
+ first.Put("a", 1);
+}
+
+TEST_CASE("Results of every kind come back", "[progmasoft][mock]")
+{
+ MockStore store;
+ std::string name = "store";
+ CATCH3_EXPECT_CALL(store.NameMock)().Returns(name);
+ CATCH3_EXPECT_CALL(store.TakeMock)().Returns(std::make_unique<int>(7));
+ CATCH3_EXPECT_CALL(store.GetMock)("throws").Throws(
+ std::runtime_error("no such key"));
+ CATCH3_EXPECT_CALL(store.GetMock)("default");
+
+ CHECK(&store.Name() == &name);
+ CHECK(*store.Take() == 7);
+ CHECK_THROWS_WITH(store.Get("throws"), "no such key");
+ CHECK(store.Get("default") == 0);
+}
+
+TEST_CASE("Actions keep their state between calls", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(next, int());
+ int count = 0;
+ CATCH3_EXPECT_CALL(next)().Times(3).Does([&count] {
+ return ++count;
+ });
+ next();
+ next();
+ CHECK(next() == 3);
+}
+
+TEST_CASE("An action may call a mock", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(outer, int());
+ CATCH3_MOCK_FUNCTION(inner, int());
+ CATCH3_EXPECT_CALL(inner)().Returns(4);
+ CATCH3_EXPECT_CALL(outer)().Does([&inner] {
+ return inner() + 1;
+ });
+ CHECK(outer() == 5);
+}
+
+TEST_CASE("Strings match by value, whatever their type", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(log, void(const char *));
+ CATCH3_MOCK_FUNCTION(size, void(std::size_t));
+ CATCH3_EXPECT_CALL(log)("text");
+ CATCH3_EXPECT_CALL(log)(nullptr);
+ CATCH3_EXPECT_CALL(size)(3);
+
+ const std::string text = "text";
+ log(text.c_str());
+ log(nullptr);
+ size(3);
+}
+
+TEST_CASE("Calls from other threads are counted", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, int());
+ CATCH3_EXPECT_CALL(tick)().Times(400).Returns(1);
+ std::vector<std::thread> threads;
+ for (int thread = 0; thread < 4; ++thread)
+ {
+ threads.emplace_back([&tick] {
+ for (int call = 0; call < 100; ++call)
+ {
+ tick();
+ }
+ });
+ }
+ for (auto &thread : threads)
+ {
+ thread.join();
+ }
+}
+
+TEST_CASE("A mock stands in for a free function behind a seam",
+ "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(clock, long());
+ CATCH3_EXPECT_CALL(clock)().Returns(50L);
+ {
+ const ScopedSeam seam(clockSeam, clock.AsFunction());
+ CHECK(SecondsSince(20) == 30);
+ }
+ CHECK(SecondsSince(0) == 0);
+
+ {
+ const ScopedSeam seam(
+ pointerSeam,
+ +[] {
+ return 9L;
+ });
+ CHECK(pointerSeam() == 9);
+ }
+ CHECK(pointerSeam == nullptr);
+}
+
+TEST_CASE("Failures of mocks fail the test case that owns them",
+ "[progmasoft][mock]")
+{
+ const Scratch scratch;
+ const auto run = RunFixture(scratch, { "--no-history", "[mock]" });
+ INFO(run.output);
+ CHECK(ExitCode(run) == 42);
+ const auto message = [&run](const std::string &name) {
+ const auto &test = run.Test(name);
+ CHECK(test["outcome"].AsString() == "failed");
+ std::string text;
+ for (const auto &assertion :
+ test["attempts"].Items().front()["assertions"].Items())
+ {
+ text += assertion["message"].AsString() + "\n";
+ }
+ return text;
+ };
+
+ const auto unexpected = message("fixture mock unexpected call");
+ CHECK_THAT(unexpected,
+ ContainsSubstring("unexpected call: Get(\"b\"), expected\n"
+ " Get(\"a\")"));
+ CHECK_THAT(message("fixture mock with no expectations"),
+ ContainsSubstring("nothing was expected of this mock"));
+ CHECK_THAT(message("fixture mock unmet expectation"),
+ ContainsSubstring("unmet expectation: tick() needs exactly 2 "
+ "calls, called once"));
+ CHECK_THAT(message("fixture mock too many calls"),
+ ContainsSubstring("too many calls: tick(), expected exactly 1 "
+ "call of tick()"));
+ CHECK_THAT(message("fixture mock out of order"),
+ ContainsSubstring("call out of order: second() came before "
+ "first()"));
+ CHECK_THAT(message("fixture mock past its step"),
+ ContainsSubstring("call out of order: first(), its step of the "
+ "sequence has passed"));
+ CHECK_THAT(message("fixture mock with nothing to return"),
+ ContainsSubstring("nothing to return from name"));
+
+ const auto leaving = message("fixture mock left by a failure");
+ CHECK_THAT(leaving, ContainsSubstring("the test's own failure"));
+ CHECK_FALSE(ContainsSubstring("unmet").match(leaving));
+}
diff --git a/tests/Progmasoft/RunnerFixture.cpp b/tests/Progmasoft/RunnerFixture.cpp
index 37d196a3..2796912f 100644
--- a/tests/Progmasoft/RunnerFixture.cpp
+++ b/tests/Progmasoft/RunnerFixture.cpp
@@ -4,6 +4,7 @@
// A test binary whose tests misbehave on purpose. The runner's integration
// tests start it and check what the runner made of each misbehaviour.
+#include <Progmasoft/Catch3/Mock.hpp>
#include <Progmasoft/Catch3/Runner.hpp>
#include <Progmasoft/Catch3/TestMacros.hpp>
#include <chrono>
@@ -184,6 +185,72 @@ TEST_CASE("fixture slow four", "[fixture][slow]")
CHECK(true);
}
+TEST_CASE("fixture mock unexpected call", "[fixture][mock]")
+{
+ Progmasoft::Catch3::MockFunction<int(std::string)> get{ "Get" };
+ CATCH3_EXPECT_CALL(get)("a").Returns(1);
+ CHECK(get("a") == 1);
+ CHECK(get("b") == 0);
+}
+
+TEST_CASE("fixture mock with no expectations", "[fixture][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, void());
+ tick();
+}
+
+TEST_CASE("fixture mock unmet expectation", "[fixture][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, void());
+ CATCH3_EXPECT_CALL(tick)().Times(2);
+ tick();
+}
+
+TEST_CASE("fixture mock too many calls", "[fixture][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, void());
+ CATCH3_EXPECT_CALL(tick)();
+ tick();
+ tick();
+}
+
+TEST_CASE("fixture mock out of order", "[fixture][mock]")
+{
+ Progmasoft::Catch3::Sequence order;
+ CATCH3_MOCK_FUNCTION(first, void());
+ CATCH3_MOCK_FUNCTION(second, void());
+ CATCH3_EXPECT_CALL(first)().InSequence(order);
+ CATCH3_EXPECT_CALL(second)().InSequence(order);
+ second();
+}
+
+TEST_CASE("fixture mock past its step", "[fixture][mock]")
+{
+ Progmasoft::Catch3::Sequence order;
+ CATCH3_MOCK_FUNCTION(first, void());
+ CATCH3_MOCK_FUNCTION(second, void());
+ CATCH3_EXPECT_CALL(first)().AtLeast(1).InSequence(order);
+ CATCH3_EXPECT_CALL(second)().InSequence(order);
+ first();
+ second();
+ first();
+}
+
+TEST_CASE("fixture mock with nothing to return", "[fixture][mock]")
+{
+ CATCH3_MOCK_FUNCTION(name, std::string & ());
+ CATCH3_EXPECT_CALL(name)();
+ name();
+ FAIL("the call above leaves the test case");
+}
+
+TEST_CASE("fixture mock left by a failure", "[fixture][mock]")
+{
+ CATCH3_MOCK_FUNCTION(tick, void());
+ CATCH3_EXPECT_CALL(tick)();
+ FAIL("the test's own failure");
+}
+
TEST_CASE("fixture skips", "[fixture][skip]")
{
SKIP("not on this machine");
diff --git a/tests/meson.build b/tests/meson.build
index 26ae225d..f6b26899 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -73,6 +73,7 @@ progmasoft_property_tests = executable(
'ProgmasoftPropertyTests',
[
'Progmasoft/Generators.tests.cpp',
+ 'Progmasoft/Mock.tests.cpp',
'Progmasoft/Property.tests.cpp',
'Progmasoft/ForAll.tests.cpp',
'Progmasoft/Results.tests.cpp',