trompeloeil icon indicating copy to clipboard operation
trompeloeil copied to clipboard

Setting expectations in a function fails

Open ninkibah opened this issue 2 years ago • 6 comments

I have a very large class I want to mock, and I created a function to set all these expectations, which I then call from the test.

Sadly, this always fails. It would appear that the ALLOW_CALL invocations create local expectation objects which need to be alive when the mock is used.

I appreciate that this might be a very large change to make to the library, but if not, this should be documented in the FAQ.

Here's my sample code:

#include <catch2/catch_test_macros.hpp>
#include <catch2/trompeloeil.hpp>

struct Real {
  virtual int func() const = 0;
  virtual ~Real() = default;
};

struct Mock : public Real {
  MAKE_MOCK0(func, int(), const);
  ~Mock() override = default;
};

void setupExpectations(Mock& obj) {
  ALLOW_CALL(obj, func()).RETURN(1729);
}

TEST_CASE("Expectations in their own function") {
  Mock mock;
  setupExpectations(mock);

  REQUIRE(mock.func() == 1729); // Throws exception saying No match for call of func with signature int() with.
}

TEST_CASE("Expectations in test") {
  Mock mock;
  ALLOW_CALL(mock, func()).RETURN(1729);

  REQUIRE(mock.func() == 1729); // This works fine, of course!
}

ninkibah avatar Sep 13 '23 13:09 ninkibah

All expectations set up are valid in the scope of the set up, so ALLOW_CALL() in setupExpectations() is alive until the end of the function setupExpectations() and therefore no longer available in the line of the comment.

You can use NAMED_ALLOW_CALL() to bind the expectation to a variable that you choose maintain the lifetime of.

rollbear avatar Sep 14 '23 04:09 rollbear

Thanks for the quick answer.

I went looking for examples with NAMED_ALLOW_CALL and didn't find any, but I guess it would look something like the folowing:

struct Real {
  virtual int func() const = 0;
  virtual ~Real() = default;
};

struct Mock : public Real {
  MAKE_MOCK0(func, int(), const);
  ~Mock() override = default;
};

auto setupExpectations(Mock& obj) {
  return NAMED_ALLOW_CALL(obj, func()).RETURN(1729);
}

TEST_CASE("Expectations in their own function") {
  Mock mock;
  auto expectation = setupExpectations(mock);

  REQUIRE(mock.func() == 1729); // Throws exception saying No match for call of func with signature int() with.
}

However, my mock needs 10-15 expectations (don't ask), so I guess I will have to create a vector of std::vector<std::unique_ptr<expectation>> and push each NAMED_ALLOW_CALL into it, and then return the vector. This is a bit ugly. I suspect that people rarely use this feature.

Tomorrow, I'll make a PR for a documentation change to help other people.

ninkibah avatar Sep 14 '23 07:09 ninkibah

That looks right. I've done it that way too. The vector is nice. I'm a bit surprised that you didn't find any examples of NAMED_ALLOW_CALL. Here's the entry in the reference manual: https://github.com/rollbear/trompeloeil/blob/main/docs/reference.md#NAMED_ALLOW_CALL, with an example. I guess there's not a good enough entry point to find it?

rollbear avatar Sep 14 '23 15:09 rollbear

I was looking in the cookbook, which seemed the best place to start. All the examples there were ALLOW_CALL or REQUIRE_CALL, so I presumed that's what I should use. I'll add a section there tomorrow, when I have time.

ninkibah avatar Sep 14 '23 15:09 ninkibah

Good to see that I hadn't found missing functionality in your library :-)

ninkibah avatar Sep 14 '23 15:09 ninkibah

Is there something I should do here, or can I close the issue?

rollbear avatar Nov 07 '23 00:11 rollbear

I know this is a very old topic and probably ready to be closed, but I think I can improve it for visitors with how we handle this.

What we did is create a 'fixture' object that handles all of this:

using expectation = std::unique_ptr<trompeloeil::expectation>;

class MockObject : public trompeloeil::mock_interface<MyInterface>
{
 // ...
}

class MockFixture
{
public:
  std::unique_ptr<MyInterface> CreateMockAndAllowAllCalls();

  MockObject myMock*{nullptr}; // stores a pointer to the mock object for future reference

  expectation mock_expect_function1;
  expectation mock_expect_function2;
  expectation mock_expect_function3;
};

std::unique_ptr<MyInterface> MockFixture::CreateMockAndAllowAllCalls()
{
  auto mock = std::make_unique<MockObject>();
  myMock = mock.get();
  mock_expect_function1= NAMED_ALLOW_CALL(*mock, Function1(_));
  mock_expect_function2= NAMED_ALLOW_CALL(*mock, Function2(_,_)).RETURN(defaultValue);
  // ...
  return mock;
}

And then, when you need to use it:

auto fix = std::make_unique<MockFixture>();
auto mock = fix->CreateMockAndAllowAllCalls();

auto sut(mock); // create object we want to test and pass it the mock.

// now all functions on mock are allowed and sut can do all it wants.

// but inside a specific testcase , we want to make sure a specific mock.function2 gets called.
// no problem:
{
  fix->mock_expect_function2 = NAMED_REQUIRE_CALL(*(fix->myMock), Function2(_,_)); // with any variation you are used to
  sut.DoSomething();
  REQUIRE(fix->is_satisfied(););
}
// obviously you should make sure that myMock is still a valid pointer. Not the best example of pointer safety, I know. 

This builds on the logic:

  • NAMED_REQUIRE_CALL and NAMED_ALLOW_CALL return a unique_ptr that lives for as long as we keep a reference to it.
  • MockFixture acts as a vessle for all the expectations.
  • We keep the expectations as public members so we can access them later. Not anonymous in a vector or map.
  • When we overwrite mock_expect_function2 the original expectation dies and the new expectation takes it place.
  • Just make sure that after overwriting the default expectation, you either end your test, or overwrite the expectation again with the default one. Otherwise test sections might become interdependent.
  • Normally everything gets cleaned up correctly here. The unwinding will first destroy sut, which pulls down mock with it. This allows sut to still call mocked functions in its destructor. After that the expectations are still alive even though there is no more mock to point to, but that should be ok since there is no more mock to call anyway. Finally all the expectations are destroyed. If the absence of mock is problematic for fixture, then you should use a shared pointer.

I hope this makes some sense. It seems to work for us.

NickvdBroeck avatar Sep 10 '25 11:09 NickvdBroeck