Search code examples
c++googletestgooglemock

gmock save argument string


I hope there is an easier way to do this... I need to capture the string which is passed as an argument to a mock.

The mock

class web_api_mock : public iweb_api
{
public:
    MOCK_METHOD(
            (bool), 
            http_post, 
            (const etl_normal_string &, const char*), 
            (override));

};

I want to capture the char * passed to the mock as second argument. I need to construct some json structure from it, and I want to check if a certain element has a certain value.

I had to spend a lot of time to get it to work, eventually copying the trick from here. This brilliant mind figured out you can rely on the gmock's Invoke.

The EXPECT_CALL

http_post_args args;

EXPECT_CALL(_web_api_mock, http_post(etl_string_equals(url), _))
    .WillOnce(
        DoAll(
            Invoke(&args, &http_post_args::capture),
            Return(true)));

Here I am invoking all arguments of the mock to a struct which I defined as follows

struct http_post_args
{
    void capture(etl_normal_string url, const char * p)
    {
        payload = std::string(p);
    }

    std::string payload;
};

And finally, I get my hands on the char * and do whatever I want afterwards.

It seems awfully complicated to save an argument when it's of the type char *.

My first attempt was the obvious mistake I guess many before (and after) me made: using the SaveArgPointee which will copy only the first element of the string and gives me with a string where the first character is correct, but the remaining string is filled with random mem.

My second attempt was to define an ACTION_P. This "almost" worked. In the callstack I could see the string I am interested in until the very last stackframe, where the args simply seem not to be passed to the actual implementation of my custom ACTION_P.

ACTION_P2(capture_string, url, payload) 
{ 
    /* if I break in the debugger, and go 1 stackframe up, 
    I can see that gmock holds my string in varargs as second element
    But I couldn't find a way to access it here*/
}

I also tried the ACTION_TEMPLATE but I am not c++ enough to understand what they are trying to explain me on gmock cookbook.

So my final question: is the above working trick with http_post_args struct really "the only way" to capture a const char * being passed as an argument to a mock?

If it SHOULD be possible using ACTION_P or ACTION_TEMPLATE would somebody be so kind to provide an actual working example with a const char *?


Solution

  • You could simply use a lambda, like so (live example):

    TEST(SomeTest, Foo)
    {
        std::string payload;
        web_api_mock m;
        EXPECT_CALL(m, http_post(Eq("url"), _))
            .WillOnce([&](const std::string &, const char* p){ 
                payload = p; 
                return true;
            });
        
        m.http_post("url", "foo string");
        EXPECT_THAT(payload, Eq("foo string"));
    }
    

    No additional http_post_args or actions etc required. Of course, you could also change the payload to a const char* if you want to "capture" the raw char pointer. But be careful with the lifetime of the pointed to characters, in this case.

    You hinted that your real code will need to parse the payload string as json and check for a certain element. It might lead to more readable tests when you create a dedicated matcher that does this. To show a rough draft (live example):

    MATCHER_P(ContainsJsonElement, expectedElement, "")
    {
        const char * payload = arg;
        // Parse payload as json, check for element, etc.
        const bool foundElement = std::string(payload) == expectedElement;
        return foundElement;
    }
    
    TEST(SomeTest, Foo) 
    {
        web_api_mock m;
        EXPECT_CALL(m, http_post(Eq("url"), ContainsJsonElement("foo string")));
        m.http_post("url", "foo string");
    }