Mocking free function

前端 未结 4 483
我在风中等你
我在风中等你 2020-11-27 06:10

I am stuck in a problem and can\'t seem to find the solution.

I am using VS2005 SP1 for compiling the code.

I have a global function:

A* foo(         


        
相关标签:
4条回答
  • 2020-11-27 06:48

    There are 2 options:

    If you insist on using gmock, there's an "extension" for global mocking from apriorit: https://github.com/apriorit/gmock-global

    It's rather limited, though - or at least I couldn't figure out in 5 minutes how to have side effects on a mocked call.

    If you're willing to switch from gmock, then hippomocks has a very neat way of doing what you want.

    Here's an example for mocking fopen, fclose and fgets for testing a member function which reads from a file using cstdio (streams are very inefficient):

    TEST_CASE("Multi entry") {
        std::vector<std::string> files{"Hello.mp3", "World.mp3"};
        size_t entry_idx = 0;
        MockRepository mocks;
        mocks.OnCallFunc(fopen).Return(reinterpret_cast<FILE *>(1));
        mocks.OnCallFunc(fgets).Do(
            [&](char * buf, int n, FILE * f)->char *{ 
                if (entry_idx < files.size())
                {
                    strcpy(buf, files[entry_idx++].c_str());
                    return buf;
                }
                else
                    return 0;
                }
            );
        mocks.OnCallFunc(fclose).Return(0);
    
        FileExplorer file_explorer;
        for (const auto &entry: files)
            REQUIRE_THAT(file_explorer.next_file_name(), Equals(entry.c_str()));
        REQUIRE_THAT(file_explorer.next_file_name(), Equals(""));
    }
    

    Where the function under test looks like this:

    string FileExplorer::next_file_name() {
        char entry[255];
        if (fgets((char *)entry, 255, _sorted_entries_in_dir) == NULL)
            return string();
        _current_idx++;
        if (_current_idx == _line_offsets.size())
            _line_offsets.push_back(static_cast<unsigned>(char_traits<char>::length(entry)) + _line_offsets.back());
        return string(entry);
    } 
    

    I'm using catch2 as the testing framework here, but I think hippomocks would work with Google's Testing framework as well (I recommend catch2, by the way, really easy to work with).

    0 讨论(0)
  • 2020-11-27 06:52

    No it's not possible, without changing the sources, or bringing your own version of foo() that is linked with the executable code.


    From GoogleMock's FAQ it says

    My code calls a static/global function. Can I mock it?

    You can, but you need to make some changes.

    In general, if you find yourself needing to mock a static function, it's a sign that your modules are too tightly coupled (and less flexible, less reusable, less testable, etc). You are probably better off defining a small interface and call the function through that interface, which then can be easily mocked. It's a bit of work initially, but usually pays for itself quickly.

    This Google Testing Blog post says it excellently. Check it out.

    Also from the Cookbook

    Mocking Free Functions

    It's possible to use Google Mock to mock a free function (i.e. a C-style function or a static method). You just need to rewrite your code to use an interface (abstract class).

    Instead of calling a free function (say, OpenFile) directly, introduce an interface for it and have a concrete subclass that calls the free function:

    class FileInterface {
     public:
      ...
      virtual bool Open(const char* path, const char* mode) = 0;
    };
    
    class File : public FileInterface {
     public:
      ...
      virtual bool Open(const char* path, const char* mode) {
        return OpenFile(path, mode);
      }
    };
    

    Your code should talk to FileInterface to open a file. Now it's easy to mock out the function.

    This may seem much hassle, but in practice you often have multiple related functions that you can put in the same interface, so the per-function syntactic overhead will be much lower.

    If you are concerned about the performance overhead incurred by virtual functions, and profiling confirms your concern, you can combine this with the recipe for mocking non-virtual methods.


    As you mentioned in your comment that you actually provide your own version of foo(), you can easily solve this having a global instance of another mock class:

    struct IFoo {
        virtual A* foo() = 0;
        virtual ~IFoo() {}
    };
    
    struct FooMock : public IFoo {
         FooMock() {}
         virtual ~FooMock() {}
         MOCK_METHOD0(foo, A*());
    };
    
    FooMock fooMock;
    
    // Your foo() implementation
    A* foo() {
        return fooMock.foo();
    }
    
    TEST(...) {
        EXPECT_CALL(fooMock,foo())
            .Times(1)
            .WillOnceReturn(new MockA());
        // ...
    }
    

    Don't forget to clear all call expectations, after each test case run.

    0 讨论(0)
  • 2020-11-27 07:05

    What has worked for me is

    • to define A* foo() in a separate source file foo.cpp in the main project,
    • not to include foo.cpp in the test project,
    • include a different source file mock-foo.cpp in the test project that provides the mock implementation of A* foo().

    For example, pseudocode for the main project file (e.g. .vcxproj or CMakeLists.txt):

    include src/foo.hpp # declare A* foo()
    include src/foo.cpp # define A* foo()
    

    and the test project file:

    include src/foo.hpp
    include test/mock-foo.cpp # define mocked A* foo()
    

    Simple and sweet, but may or may not work in your case.

    0 讨论(0)
  • 2020-11-27 07:10

    Of course, the answer explaining the solution according to GTest/GMock's documentation couldn't be much more correct.

    But I would like to add a temporary quick&dirty approach. It should be applicable to cases where you want to get legacy C/C++ code under test as quickly and as non-invasively as possible. (Just to proceed with fixes, refactoring and more proper testing as soon as possible after.)

    So, to mock a free function void foo(int) appearing in some code to be tested, within the source file you just make the following adaptions:

    #if TESTING
    #define foo(param) // to nothing, so calls to that disappear
    #endif
    
    // ... code that calls foo stays untouched and could be tested
    

    The macro TESTING, indicating that the code runs under test, doesn't come with GTest/GMock - you need to add it to test targets by yourself.

    The possibilities are rather limited, but you might also be able to construct something useful for return types as A* in the question's example.

    Unfortunately, also this isn't a solution without changing the code. If that is really necessary, you could Google for 'link seams'. But my guess is that this could be quite a hassle in practice. And it even might not be possible at all in many/most cases?!

    0 讨论(0)
提交回复
热议问题