Friday, February 17, 2012

Mock me all you want...

So today is a pretty big day for having my infrastructure in place for Phoenix. The source code is kept in Git, with code reviews being done by Gerrit. For code verification, Jenkins is the tool of choice, and of course, Jenkins is also handling the mainline branches on a per-changelist basis.

So now to talk a bit more about mocking. Or, in particular, mock objects. The architecture is in place, many of the design issues have been hashed over, and it's time to start putting a few pieces of the puzzle into place. Developing for a large variety of devices is difficult, and developing on the device itself is even trickier. But fortunately, because of the object-oriented nature of Phoenix, this task has been greatly simplified with a combination of mock objects and cxxtest. So what is the purpose of a mock objects? Let me show with a simple example.

Let's use an example of the block reader/writer design. Normally, on a device, the block reader/writer will handle all direct read and write requests to the flash memory. But how do I test the classes which use the block reader/writer without messing up the devices? What if there's a bug?

class MockBlockReaderWriter : public IBlockReaderWriter
{
public:
  int readBlock(unsigned long int blockNum, void* block)
  {
    if (blockNum > 31)  return 1;
    memcpy(block, mBlocks[blockNum]);
    return 0;
  }

  int writeBlock(unsigned long int blockNum, void* block)
  {
    if (blockNum > 31)  return 1;
    memcpy(mBlocks[blockNum], block);
    return 0;

  }

  int getBlockSize(void) { return 512; }
private:
  unsigned char mBlocks[32][512];
};

This is a mock object. Now, to do this better, I'd pre-initialize the data in the blocks to something (anything) which allows me to recognize unread sections. I might even add special 'test' APIs that allow my unit tests to verify that all blocks were read or written, etc.

Now, I write three more blocks of code... The first, a unit test around the mock block reader/writer. Why? Because I need to know that it does exactly what I'm expecting it to do. This will be identical to a unit test I write to verify the *real* block reader/writer, so I'm really killing two birds with one stone. I'll just make the test case smart in that it takes in the type of block reader/writer to test with, and verifies full functionality.

The second block of code to write is the real code for one of the classes that uses the block reader/writer. Since I have a block reader/writer that I can test with, I can write real code without even having access to a real block reader/writer available.

And finally, I write unit tests for *that* class. If that class needs one, I'll also write a mock object for that, so that other classes can be written and tested without needing a real interface for it.

Now, how does this affect development time? It shortens it. I'm sure that sounds backwards, but the reality is, with a good build system and great unit test framework, the time it takes to test the code is greatly reduced, and more importantly, the amount of time trying to debug strange issues that could potentially brick our limited number of test devices. Because the unit tests run under the host OS, they can be debugged using normal debugging tools, making quick work of large numbers of bugs.

So as we start to see those empty branches populating with more and more code chunks, we can sleep easily at night knowing that Jenkins is on it, making sure our trees remain green and our sodas remain caffeinated.

1 comment: