Search code examples
c++polymorphismsmart-pointersgooglemock

How to use gmock SaveArgPointee with with std::shared_ptr of derived class


I have a BaseMessage class from which I derive several different DerivedMessage subclasses and want to send them like this:

class BaseMessage {
public:
   virtual std::vector<uint8_t> data() const noexcept = 0;
   virtual ~BaseMessage() = default;
   [...]
}

class DerivedMessage : public BaseMessage {
public:
   [...]
   std::vector<uint8_t> data() const noexcept override { return m_data; }
private:
   std::vector<uint8_t> m_data;
}

// simplified 
class Tcp {
public
   virtual void sendMessage(std::shared_ptr<BaseMessage> msg) { write(msg->data());}
   [...]
};

class SomeClass {
public:
   SomeClass(Tcp& tcp) : m_tcp(tcp) {}
   void writeDataToRemote(std::shared_ptr<DerivedMessage> derived) const {
      m_tcp.sendMessage(derived);
private:
   Tcp m_tcp;
}
};

Now I want to write tests for SomeClass with gtest.

Therefore I mock the function of the TCP class:

class MockTcp : public Tcp {
MOCK_METHOD(void, sendMessage, (std::shared_ptr<ralco::CommandMsg> msg), (override));
[...]
}

Let's assume that all is simplified up to here but works.

So in the test, I'd like to inspect the argument given to sendMessage in the function writeDataToRemote. I read about ::testing::SaveArg and ::testing::SaveArgPointee on StackOverflow (but not in the documentation, though).

TEST(SomeClassTest, writesMessageToSocket){
   MockTcp mockTcp;
   SomeClass sc(mockTcp);
   
   // >>>how to declare msgArg here?<<<
   EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(::testing::SaveArgPointee<0>(msgArg));
   const auto derivedMsg = std::make_shared<DerivedMessage>();
   sc.writeDataToRemote(derivedMsg);
   // further inspection of msgArg follows
}

As written in the code comment, I don't know how to declare the msgArg variable so that it can be assigned the actual argument given to sendMessage. When using SaveArg I think I'd get a dangling pointer and doing it as above I get errors because the message cannot be copy-assigned. Any hints apreciated.


Solution

  • In your case you actually want to just save and inspect the considered shared_ptr, so it is enough to use SaveArg:

        MockTcp mockTcp;
        SomeClass sc(mockTcp);
        std::shared_ptr<BaseMessage> bm;
        EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(::testing::SaveArg<0>(&bm));  // it will effectively do bm = arg;
        const auto derivedMsg = std::make_shared<DerivedMessage>();
        sc.writeDataToRemote(derivedMsg);
        // verify that the argument that you captured is indeed pointing to the same message
        std::cout << derivedMsg.get() << std::endl;
        std::cout << bm.get() << std::endl;
    

    The common misunderstanding of SaveArgPointee is that it assigns value pointed by the arg to your local variable in test, which in your case maybe is not a good idea, because it would invoke a copy constructor of Message.

    Alternatively I can recommend using Invoke. It is very generic and easy to use. You can e.g. capture the desired argument like that:

        MockTcp mockTcp;
        SomeClass sc(mockTcp);
        std::shared_ptr<BaseMessage> bm;
        EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(::testing::Invoke([&bm](auto arg) { bm = arg; }));
        const auto derivedMsg2 = std::make_shared<DerivedMessage>();
        sc.writeDataToRemote(derivedMsg2);
        std::cout << derivedMsg2.get() << std::endl;
        std::cout << bm.get() << std::endl;
    

    Or using a raw pointer to BaseMessage:

        BaseMessage* rawBm = nullptr;
        EXPECT_CALL(mockTcp, sendMessage(_)).Times(1).WillOnce(Invoke([&rawBm](auto arg) { rawBm = arg.get(); }));
        const auto derivedMsg2 = std::make_shared<DerivedMessage>();
        sc.writeDataToRemote(derivedMsg2);
        std::cout << derivedMsg2.get() << std::endl;
        std::cout << rawBm << std::endl;