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

summaryrefslogtreecommitdiffhomepage
path: root/tests/Progmasoft/Mock.tests.cpp
diff options
context:
space:
mode:
authorLeitwolf2026-11-01 21:19:33 +0300
committerGitHub2026-11-01 21:19:33 +0300
commit5cbd481125b881a4c2eed74050be9899eb3f23a7 (patch)
tree496d26ff659e92cdf4e6fe27a4aa47bf52818052 /tests/Progmasoft/Mock.tests.cpp
parent26a1588ae3ae183ae5c900caf3c294bdcb3f8855 (diff)
parent8603d31fd8c1a72e30cb444f278d7a0592383ba7 (diff)
downloadcatch3-5cbd481125b881a4c2eed74050be9899eb3f23a7.tar.gz
Merge pull request #30 from Progmasoft/feature/mocking
Add mocks that report through the assertion path
Diffstat (limited to 'tests/Progmasoft/Mock.tests.cpp')
-rw-r--r--tests/Progmasoft/Mock.tests.cpp405
1 files changed, 405 insertions, 0 deletions
diff --git a/tests/Progmasoft/Mock.tests.cpp b/tests/Progmasoft/Mock.tests.cpp
new file mode 100644
index 00000000..a5213e51
--- /dev/null
+++ b/tests/Progmasoft/Mock.tests.cpp
@@ -0,0 +1,405 @@
+// 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 <atomic>
+#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);
+ };
+
+ /// A mock with no base class, and so nothing after its parameters.
+ class MockCounter final
+ {
+ public:
+ CATCH3_MOCK_METHOD(int, Twice, (int));
+ };
+
+ /// A seam production code calls a free function through.
+ std::function<long()> &
+ ClockSeam()
+ {
+ static auto *seam = new std::function<long()>{ [] {
+ return 0L;
+ } };
+ return *seam;
+ }
+
+ 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("A mock method needs nothing after its parameters",
+ "[progmasoft][mock]")
+{
+ MockCounter counter;
+ CATCH3_EXPECT_CALL(counter.TwiceMock)(2).Returns(4);
+ CHECK(counter.Twice(2) == 4);
+}
+
+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("A null text expectation accepts only null", "[progmasoft][mock]")
+{
+ const char *const missing = nullptr;
+ CATCH3_MOCK_FUNCTION(pointer, int(const char *));
+ CATCH3_MOCK_FUNCTION(text, int(const std::string &));
+ CATCH3_EXPECT_CALL(pointer)(missing).Returns(1);
+ CATCH3_EXPECT_CALL(pointer)("text").Returns(2);
+ CATCH3_EXPECT_CALL(text)(missing).AnyTimes().Returns(1);
+ CATCH3_EXPECT_CALL(text)(Any).Returns(2);
+
+ CHECK(pointer(nullptr) == 1);
+ CHECK(pointer("text") == 2);
+ CHECK(text("") == 2);
+}
+
+TEST_CASE("Integers match by value, whatever their types", "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(narrow, int(unsigned char));
+ CATCH3_MOCK_FUNCTION(unsignedOne, int(unsigned));
+ CATCH3_MOCK_FUNCTION(signedOne, int(int));
+ CATCH3_MOCK_FUNCTION(flag, int(bool));
+ CATCH3_EXPECT_CALL(narrow)(256).AnyTimes().Returns(1);
+ CATCH3_EXPECT_CALL(narrow)(Any).Returns(2);
+ CATCH3_EXPECT_CALL(unsignedOne)(-1).AnyTimes().Returns(1);
+ CATCH3_EXPECT_CALL(unsignedOne)(7).Returns(2);
+ CATCH3_EXPECT_CALL(unsignedOne)(Any).Returns(3);
+ CATCH3_EXPECT_CALL(signedOne)(4294967295U).AnyTimes().Returns(1);
+ CATCH3_EXPECT_CALL(signedOne)(Any).Returns(2);
+ CATCH3_EXPECT_CALL(flag)(true).Returns(1);
+
+ CHECK(narrow(0) == 2);
+ CHECK(unsignedOne(7U) == 2);
+ CHECK(unsignedOne(4294967295U) == 3);
+ CHECK(signedOne(-1) == 2);
+ CHECK(flag(true) == 1);
+}
+
+TEST_CASE("Calls from other threads share a result that can only move",
+ "[progmasoft][mock]")
+{
+ CATCH3_MOCK_FUNCTION(take, std::unique_ptr<int>());
+ CATCH3_EXPECT_CALL(take)().Times(400).Returns(std::make_unique<int>(1));
+ std::atomic<int> taken{ 0 };
+ std::vector<std::thread> threads;
+ for (int thread = 0; thread < 4; ++thread)
+ {
+ threads.emplace_back([&take, &taken] {
+ for (int call = 0; call < 100; ++call)
+ {
+ if (take() != nullptr)
+ {
+ ++taken;
+ }
+ }
+ });
+ }
+ for (auto &thread : threads)
+ {
+ thread.join();
+ }
+ CHECK(taken == 1);
+}
+
+TEST_CASE("A mock can outlive the run", "[progmasoft][mock]")
+{
+ const Scratch scratch;
+ const auto run
+ = RunFixture(scratch, { "--no-history", "[mock-exit]", "--jobs=1" });
+ INFO(run.output);
+ CHECK(ExitCode(run) == 0);
+ CHECK(run.Test("fixture mock outlives the run")["outcome"].AsString()
+ == "passed");
+}
+
+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));
+}