Channels ▼
RSS

Design

Testing OO Systems, Part 1


Mocking

Let's look at how this principal plays out in the context of a few simple tests. (None of the code from the past few months has needed anything particularly sophisticated by way of tests). All these tests are based on JUnit, which has been around long enough that I don't feel to compelled to explain it, here. If you've never used JUnit (or an equivalent testing framework), then you should start immediately. Get JUnit from junit.org. The main things you'll need to know are that four annotations determines how the test suite runs:

@BeforeClass Called once, before any tests run
@AfterClass Called once, after all tests run
@Before Called before each test is run
@After Called after each test is run
@Test Indicates a test method that's called automatically by the "test-runner" application. You won't call this method explicitly.

The rest of JUnit is made up of various self-explanatory "assert" variants that will cause the test to fail if the argument is false. Both Eclipse and IntelliJ IDEA recognize the earlier annotations and let you run the tests from inside the development system.

The tests also use two mocking frameworks in a very lightweight way: Mockito and PowerMock. PowerMock is really a Mockito (and EasyMock) extension that adds the ability to mock static and private things. I'll discuss both of these frameworks in depth next month, but here's the essentials:

A mocking framework gives you the ability to do two things:

  1. When you "mock" an interface, the framework essentially creates a class for you that implements the interface. The created methods typically behave simplistically, but are good enough for most uses. If you need to, you can replace the machine-generated method with one of your own. If you mock a class, as compared to an interface, the mock is actually an instance of a derived class that's created by the framework at runtime. The machine-generated derived class overrides all the public base-class methods with mock versions. Traditional Test-Driven Development folks frown on mocking classes, but TDD and mocking are not synonymous. There are perfectly good reasons to mock a class. We'll see some examples next month.
  2. When you "spy" on an existing object, you essentially wrap (rather than extend) the object. The "spy" (the wrapper) methods typically just call the methods with the same name in the wrapped object (the thing you're spying on), but record things like how many times the method was called and what the arguments were. This way, you can surround the object under test with real clients while being able to monitor how the object under test interacts with those clients.

I'll show you examples of how all this is done, below.

Testing Locations and Places

The Places class from my Java Apps article is tested by LocationsAndPlacesTest in the code sample LocationsAndPlacesTest.java.

The class is annotated with the following lines:

@RunWith(PowerMockRunner.class) 
@PowerMockIgnore("org.apache.log4j.*") 
@PrepareForTest({Places.class, LocationsSupport.class}) 

RunWith is a JUnit annotation that tells the system to use a special PowerMockRunner class to run the tests instead of using the default JUnit test runner. PowerMock has to do a bit of quite complicated magic to override static methods (it has to get hold of the class-loading process and dynamically create a wrapper around the real class at load time), and the special runner performs that magic. The PowerMockIgnore annotation tells the custom runner that it shouldn't provide a wrapper around any of the log4j classes, which get very confused if they're wrapped. (You end up with two versions of the Logger class, one loaded by the default Java class loader and the other loaded by the PowerMock loader. This just doesn't work.) The PrepareForTest statement tells the PowerMock loader which classes it has to wrap. That is, it specifies the classes that contain static methods that we want to mock.

The actual mocking is done in the test methods. Here's a sample (I've added comments to explain what's going on):

 @Test
    public void testInitializationFromSystemProperty()
    {
        // Tell PowerMock that we will be spying on the System class. PowerMock will
        // provide a transparent wrapper around System, and we'll be able to control
        // the behavior of the wrapper methods. If we don't do anything in particular,
        // then the wrapper method calls the default System-class method.
        //
        spy( System.class );

        // Tell the transparent wrapper to return the String "/tmp" whenevner
        // someone calls System.getProperty("HOME");
        //
        when(System.getProperty("HOME")).thenReturn("/tmp");

        // Tell the transparent wrapper to return the String "/tmp" whenevner
        // someone calls System.getenv("HOME");
        //
        when(System.getenv("HOME")).thenReturn(null);

        // Now actually run the test. assertEquals() is a JUnit method that causes the test to
        // fails if the arguments aren't equal.

        assertEquals( new File("/tmp"), Places.HOME.directory()  );
    }

When this code runs, the Places.HOME.directory() method runs in the normal way. That method normally gets the HOME directory, first by looking for a -D JVM-command-line argument (returned by System.getProperty()), and if that property isn't found, by looking for an environment variable called HOME. In the current test, however, Places.HOME will call our mock methods (of the System class) rather than calling the real System-class methods, and our mocks return /tmp. That is, rather than requiring us to set up an environment variable before running the test, the test supplies it's own values for System variables. That way the test doesn't require any prior setup (which can easily go wrong), particularly in an automated-build environment. This way of working — where the tests do all their own setup work — make it vastly easier to do things like continuous integration, since the scripts run by the integration system (which would otherwise need to set up the test environment) are both simpler, and don't have to be modified if we add or modify tests.

Now, thinking back to the first part of the current article, notice how the Places object is being tested as a black box. I'm modifying the behavior of the immediate neighbors of the object that I'm testing (System), asking the object to do something (Places.HOME.directory()) and then observing the behavior of the object (it's return the expected value when asked). I don't look at the internal state of the object under test at all. In fact, the object under test doesn't even know it's being tested. It certainly doesn't know that I've changed the way the System object works.

Most of the other tests work in the same way.

The next thing to notice are the tests at the bottom of the file, which all test that the Places fails correctly when something's wrong. It's never sufficient simply to test that things work as expected; we also need to test that things fail when expected. For example, the following test method succeeds when an IllegalStateException is thrown. The test is setting up a failure condition: It creates a temporary file, then, using mocks, it tells the System object to return the name of that temporary file when Places requests the TMP environment. The call to Places.TMP.directory(); should now fail because TMP references a file, not a directory. The @Test(expected=IllegalStateException.class) tells JUnit to treat that exception toss as a success.

// Should fail because we the name is in use for a file.
    @Test(expected=IllegalStateException.class)
    public void testExistsButNotDirectory() throws IOException
    {
        File temp = File.createTempFile("test", "tmp", new File("/tmp") );
        temp.deleteOnExit();
        
        spy( System.class );
        when(System.getProperty("TMP")).thenReturn( LocationsSupport.asString(temp) );
        when(System.getenv     ("TMP")).thenReturn( LocationsSupport.asString(temp) );
        Places.TMP.directory();
    }
    


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video