Mocking file output for unit testing

Having a unit test harness makes modifying code a lot easier, because it lets you quickly spot anything you've broken as you're coding. But when you've got legacy code that doesn't have unit tests, getting the code into a test harness can be a lot of work. You've got code connecting to databases, touching the file system, and interacting with the GUI all over the place.

In order to get that kind of code into a test harness, I first create "characterization tests" — tests that simply verify what the code is doing. I let the code write files, connect to databases, etc. Then I gradually modify them to create a layer between the code and the outside world, which I can then manipulate for testing.

Here's a very simple example. I have a ConfigWriter class, which (duh) writes config files. The class lets you set and get various properties, and then write the config file, specifying a file name.

class ConfigWriter
{
    map<string, string> m_props ;
public:
    void set_prop(const string &key, const string &val) ;
    string get_prop(const string &key) ;
    void write(const string &filename) ;
} ;

The class has been doing great, but now I want to modify it. Maybe I want to add functionality, or maybe I've found a bug. At any rate, before I make the changes I want to get this class into a test harness — but I want to be able to unit test it without touching the file system.

My first step is to write a unit test that confirms what the class actually does. I use EasyUnit as my unit-testing framework, but you can use any framework you're happy with.

TEST( TestConfigWriter, write )
{
    ConfigWriter writer ;
    writer.set_prop("files", "3") ;
    writer.set_prop("drives", "1") ;

    string fname = "/tmp/config_test1.ini" ;
    writer.write(fname) ;
    string expected = "files=3\r\ndrives=1" ;
    // a helper class that creates a memory-mapped file
    MMFile mm_file ;
    string actual = mm_file.create_view(fname) ;
    ASSERT_EQUALS_V(expected, actual) ;
}

I run the test, and it passes. Now I add a new write method, which takes a pointer to an ostream instead of a file name.

class ConfigWriter
{
    map<string, string> m_props ;
public:
    void set_prop(const string &key, const string &val) ;
    string get_prop(const string &key) ;
    void write(const string &filename) ;
    void write(ostream *writer) ;
} ;

I run my unit tests, and make sure that they still work. Then I add a new unit test to test the new method:

TEST( TestConfigWriter, write_stream )
{
    ConfigWriter writer ;
    writer.set_prop("files", "3") ;
    writer.set_prop("drives", "1") ;

    string fname = "/tmp/config_test1.ini" ;
    ofstream stream ;
    stream.open(fname.c_str()) ;
    writer.write(&stream) ;
    stream.close() ;
    string expected = "files=3\r\ndrives=1" ;
    // a helper class that creates a memory-mapped file
    MMFile mm_file ;
    string actual = mm_file.create_view(fname) ;
    ASSERT_EQUALS_V(expected, actual) ;
}

Once I make sure they pass, I modify the write(const string &fname) method so that it calls the ostream version.

void ConfigWriter::write(const string &filename)
{
    ofstream stream ;
    stream.open(filename.c_str()) ;
    this->write(&stream) ;
    stream.close() ;
}

I run the unit tests, and make sure they all pass.

Now I'm ready to mock the file output. I change the ofstream in my unit test to an ostringstream:

TEST( TestConfigWriter, write_stream )
{
    ConfigWriter writer ;
    writer.set_prop("files", "3") ;
    writer.set_prop("drives", "1") ;

    ostringstream stream ;
    writer.write(&stream) ;
    string expected = "files=3\r\ndrives=1" ;
    // look ma, we didn't touch the file system!
    string actual = stream.str() ;
    ASSERT_EQUALS_V(expected, actual) ;
}

I run the unit tests again. Now I'm able to unit test the class without touching the file system. I delete the unit test for the write(const string &fname) method.

I'll still have regression and function tests that actually write config files and test their output, but I want my unit tests to run very quickly, without affecting the "outside world."

Leave a Reply

 

 

 

You can use these HTML tags

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>