Boost C++ Libraries Home Libraries People FAQ More

PrevUpHomeNext

Mocking Collaborators

In testing file I/O, we showed an example of creating a class fake_directory_scanner that acted as a simple test double for an interface. Such simple fake objects are fine at first, but they're full of boring boiler plate code and after a while it becomes tedious to write them by hand.

For collaborators defined by a pure virtual base class, also known as an interface, there are mock object frameworks that alleviate the need to write the repetitive boiler plate code. One such mock object framework that is designed to work with Boost.Test is the Turtle mock object framework.

Here is an example of the file system tests written using turtle:

#include "directory_scanner.hpp"
#include <turtle/mock.hpp>

MOCK_BASE_CLASS(mock_directory_scanner, directory_scanner)
{
    MOCK_METHOD(begin, 1);
    MOCK_METHOD(has_next, 0);
    MOCK_METHOD(next, 0);
};

Defining the mock object is considerably simpler than writing a fake object by hand. Turtle uses variadic argument macros and template based type deduction to infer the type signature of the methods simply from the number of arguments each method takes. This alleviates us from having to repeat all of this information from the interface into the implementation of the test double.

Now we can rewrite our tests to use a mock object configured for each test case:

static std::string const ARBITRARY_DIRECTORY_NAME("foo");
static std::string const ARBITRARY_NON_TEXT_FILE_NAME("foo.foo");
static std::string const ARBITRARY_TEXT_FILE_NAME("foo.txt");
static std::string const ARBITRARY_OTHER_TEXT_FILE_NAME("bar.txt");

struct text_files_fixture
{
    text_files_fixture()
    {
        MOCK_EXPECT(scanner.begin).once().with(ARBITRARY_DIRECTORY_NAME);
    }

    void expect_enumerate_non_text_file()
    {
        MOCK_EXPECT(scanner.has_next).once().returns(true);
        MOCK_EXPECT(scanner.next).once().returns(ARBITRARY_NON_TEXT_FILE_NAME);
    }

    void expect_enumerate_text_file(std::string const& file_name = ARBITRARY_TEXT_FILE_NAME)
    {
        MOCK_EXPECT(scanner.has_next).once().returns(true);
        MOCK_EXPECT(scanner.next).once().returns(file_name);
        expected.push_back(file_name);
    }

    void expect_enumerate_end()
    {
        MOCK_EXPECT(scanner.has_next).once().returns(false);
    }

    mock_directory_scanner scanner;
    std::vector<std::string> empty;
    std::vector<std::string> expected;
};

BOOST_FIXTURE_TEST_SUITE(test_text_files, text_files_fixture);

BOOST_AUTO_TEST_CASE(returns_empty_for_empty_directory)
{
    expect_enumerate_end();

    std::vector<std::string> files = text_files(ARBITRARY_DIRECTORY_NAME, scanner);

    BOOST_REQUIRE_EQUAL_COLLECTIONS(empty.begin(), empty.end(), files.begin(), files.end());
}

BOOST_AUTO_TEST_CASE(returns_empty_for_no_text_files)
{
    expect_enumerate_non_text_file();
    expect_enumerate_end();

    std::vector<std::string> files = text_files(ARBITRARY_DIRECTORY_NAME, scanner);

    BOOST_REQUIRE_EQUAL_COLLECTIONS(empty.begin(), empty.end(), files.begin(), files.end());
}

BOOST_AUTO_TEST_CASE(returns_file_for_text_file)
{
    expect_enumerate_text_file();
    expect_enumerate_end();

    std::vector<std::string> files = text_files(ARBITRARY_DIRECTORY_NAME, scanner);

    BOOST_REQUIRE_EQUAL_COLLECTIONS(expected.begin(), expected.end(), files.begin(), files.end());
}

BOOST_AUTO_TEST_CASE(returns_only_text_file_for_mixed_files)
{
    expect_enumerate_text_file();
    expect_enumerate_non_text_file();
    expect_enumerate_end();

    std::vector<std::string> files = text_files(ARBITRARY_DIRECTORY_NAME, scanner);

    BOOST_REQUIRE_EQUAL_COLLECTIONS(expected.begin(), expected.end(), files.begin(), files.end());
}

BOOST_AUTO_TEST_CASE(returns_all_text_files)
{
    expect_enumerate_text_file();
    expect_enumerate_non_text_file();
    expect_enumerate_text_file(ARBITRARY_OTHER_TEXT_FILE_NAME);
    expect_enumerate_end();

    std::vector<std::string> files = text_files(ARBITRARY_DIRECTORY_NAME, scanner);

    BOOST_REQUIRE_EQUAL_COLLECTIONS(expected.begin(), expected.end(), files.begin(), files.end());
}

BOOST_AUTO_TEST_SUITE_END();

The tests themselves are largely unchanged and what is different is the intention revealing methods we placed in the test fixture. Mock object frameworks generally provide a rich set of methods on the mock object for setting the expectations on the number of times a method is called, such as once(), the expected arguments to a method, such as with(), and the return value of a method, such as returns().

With a turtle mock, the expectations are verified when the mock object is destroyed. The mock is a member in our fixture and will be destroyed at the end of every test case.

Example Source Code

PrevUpHomeNext