Unit testing and the reason for mocking

If you’ve read my definitions of unit testing and system testing in my Ten Tenets of Test Automation, you’ll know that the primary differences between the two are target audience and granularity of coverage. Unit tests seek to validate the intentions of developers at a very fine, narrow scope, often at the function or small class level.

There should be lots of these fine-grained checks to validate your code base. Having them helps you understand the impact of evolving requirements. Having to deal with change in the life of developing a product is as certain as the sun rising every day. Even the best developers struggle to assess the compatibility of their new thoughts and assumptions with ones previously made.

The necessary, fine-grained nature of unit testing forces the sacrifice of a realistic, holistic test (i.e. an integration/system test) for one that isolates the unit-under-test from all external dependencies. This isolation is necessary for fast developer feedback and assessing the impact of new ideas on previously made intentions, without any dependency that the unit-under-test has no control over. The most common approach to isolating the unit-under-test is to mock those dependencies.

Mocking with architecture – should you?

There are a variety of approaches to mocking. In a highly decoupled system that separates code interface from implementation, you can achieve it with architectural techniques like Dependency Injection or by employing a variety of design patterns, e.g. Repository, if your dependency is in the area of data access. You can develop backing implementations of these dependencies that are more suited to unit testing, but not used in production.

However, an argument that has gained some traction in recent years is that introducing additional levels of indirection solely for the purpose of testing leads to over-engineering and complicates the code base unnecessarily, damaging clarity and readability [DHH]. Although there are plenty of big name developers that disagree with that view and believe that strict adherence to the principles of test-driven development will always lead you towards a better, more flexible architecture overall [Uncle Bob].

My view on that argument is that both groups are right under certain circumstances. I would introduce an architectural solution (and pay the arguable cost of reduced clarity) if there was a moderate chance that my dependency would need to change over time, or my product would need to scale over several providers of that chunk of functionality. For me, these are primarily motivated by the risk of change, not testability. I consider this to be a risk assessment exercise, making the correct decision for you is entirely dependent on your circumstances and how you perceive the risks.

Mocking without generalization

If you have a need to isolate a dependency and don’t want to apply an architectural solution, there are many great mocking frameworks out there to help you, even down to the level of mocking individual functions on a class you do not own. These can give you the isolation your unit testing requires without having an impact (positive or negative) on your architecture or design.

A real life situation

Something I’ve been working on recently is a Jenkins plugin which can download a bunch of artifacts from a CIFS/SMB intranet file system to the Jenkins workspace and run a job with them. I’m using JCIFS, an open source CIFS client library for Java, in the plugin. My unit tests can’t use an intranet file system directly, not only because such a thing would be a production system, but also because it’s an infrastructure requirement and inherently not portable. A big no-no in unit testing.

I needed to mock my plugin’s interactions with JCIFS, but there was virtually no risk of me needing to support other intranet file systems. The plugin is built for CIFS/SMB, not to be a general framework for doing this sort of thing, so an architectural solution is overkill.

Enter Mockito

Part of the plugin is responsible for applying a set of exclusion filters to the file tree to exclude known subdirectories/files that are not useful to the Jenkins job. I need to retrieve the names and UNC paths of the remote files to see if they match any configured exclusion filters.

Mockito is a handy mocking framework for Java that made it easy to override the functions on JCIFS’ SmbFile class that would have required a real remote filesystem to use. Here’s a snippet of setup code from the JUnit suite responsible for testing UNC path matching:

public class MyJUnitTestSuite {

    private class MockSmbFileData {
        public String name;
        public String uncPath;
        public MockSmbFileData[] children;

        SmbFile getMockSmbFile() {
            // Depth-first traversal of child SmbFiles
            SmbFile[] mockChildren = new SmbFile[children.length];
            for(int i = 0; i < children.length; ++i) {
                mockChildren[i] = children[i].getMockSmbFile();
            }

            // Create mock SmbFile (from JCIFS) class
            SmbFile result = mock(SmbFile.class);

            // Stub the getName and getUncPath methods to return fake data.
            when(result.getName()).thenReturn(name);
            when(result.getUncPath()).thenReturn(uncPath);

            // Stub listFiles method to return child SmbFiles like a real file structure.
            // Stub isFile to return true if this is a leaf node.
            try {
                when(result.listFiles()).thenReturn(mockChildren);
                when(result.isFile()).thenReturn(mockChildren.length == 0);
            } catch (SmbException e) {
                // This is a mock SmbFile so just snaffle the exception.
            }

            return result;
        }
    }

    @Rule
    public TemporaryFolder temp = new TemporaryFolder();
    private MockSmbFileData dataRoot;
    private SmbFile rootDirectory;

    public MyJUnitTestSuite () throws IOException, URISyntaxException {

        // Read JSON representation of mock SMB file structure and build a tree from it.
        URL jsonLocation = MyJUnitTestSuite.class.getResource("MockSmbFileData.json");
        String json = new String(Files.readAllBytes(Paths.get(jsonLocation.toURI())));

        // JSON deserialization maps JSON data directly into a MockSmbFileData tree.
        Gson gson = new Gson();
        dataRoot = gson.fromJson(json, MockSmbFileData.class);

        // getMockSmbFile generates an SmbFile structure with stubbed methods for our tests (see above).
        rootDirectory = dataRoot.getMockSmbFile();
    }

    @Test
    public void test() throws IOException, InterruptedException {
        // snipped
    }

The Mockito methods at play here are:

  • mock(SmbFile.class) line 16: Builds a fake version of the given class that you can attach method stubs to.
  • when(SmbFile.method()).thenReturn(data) lines 19, 20, 25 & 26: Defines what data to return when the given method is called on the mock.

A high-level walkthrough of the suite setup:

  1. Read a known JSON file containing a representation of a fake CIFS/SMB file system.
  2. Deserialize it into MockSmbFileData, a tree that represents what data each fake SmbFile should return for it to act like a real file system, i.e. what UNC paths to return for each node when getUncPath() is called, what children each node has when listFiles() is called, etc.
  3. Traverse the MockSmbFileData tree to generate a mock SmbFile tree that mirrors it.

From that point on, all my unit tests in this test suite think they’re operating on a real intranet file system.

For reference, here’s the JSON file:

{
  "name": "RootPath",
  "uncPath": "\\\\RootPath\\",
  "children": [
    {
      "name": "A.txt",
      "uncPath": "\\\\RootPath\\A.txt",
      "children": []
    },
    {
      "name": "1",
      "uncPath": "\\\\RootPath\\1\\",
      "children": [
        {
          "name": "A.txt",
          "uncPath": "\\\\RootPath\\1\\A.txt",
          "children": []
        },
        {
          "name": "B.txt",
          "uncPath": "\\\\RootPath\\1\\B.txt",
          "children": []
        }
      ]
    },
    {
      "name": "2",
      "uncPath": "\\\\RootPath\\2\\",
      "children": [
        {
          "name": "A.txt",
          "uncPath": "\\\\RootPath\\2\\A.txt",
          "children": []
        },
        {
          "name": "1",
          "uncPath": "\\\\RootPath\\2\\1\\",
          "children": [
            {
              "name": "A.txt",
              "uncPath": "\\\\RootPath\\2\\1\\A.txt",
              "children": []
            },
            {
              "name": "B.txt",
              "uncPath": "\\\\RootPath\\2\\1\\B.txt",
              "children": []
            }
          ]
        }
      ]
    }
  ]
}

I found this to be quite an elegant solution for assembling a mock file system for testing my plugin’s behaviors. To introduce new types of UNC path, all I need to do is modify the JSON representation to reflect what I want to test, which is quite neat.

Words of warning

Like all useful techniques, you can take mocking too far. With too much mocking, you risk running tests that don’t reflect production behavior any more or mocks that don’t keep up with the changing behavior of those dependencies over time. Be mindful of the risks this can introduce, it can be very easy for tests to start reporting false-positives (a test that passes but doesn’t catch the intended production failure) when this happens. A test that reports a false-positive can be worse than having no test at all.

In my case, I’m dealing with primitive types like strings and booleans and the interfaces to the functions I am stubbing are at very low risk of changing. I’m not mocking the behavior of algorithms or anything else that is likely to change much going forward. Be mindful that elaborate mocks can have bugs in them, like all other code.

Closing thoughts

I hope you can see the value of having a mocking framework like Mockito in your toolbox and that I’ve helped you think about when and where such a technique should be applied.

When you are unit testing, consider the architectural choices that would facilitate isolation/testability and whether they would be appropriate for your situation. Sometimes they can lead you to a better design that stands up well to future change and sometimes it isn’t worth the extra complexity. For the latter situations, consider if Mockito or a similar tool can help you.

About the Author Kirk MacPhee

An experienced software developer and technical lead, specializing in automation technologies and their application.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s