Menu

Mock objects on Android with Borachio: Part 2

In part 1 of this series, I showed how to mock an interface that we created ourselves under Android. That’s useful, but mocking really pays dividends when mocking OS services—doing so allows us to test our code in isolation, verify that it interacts with the OS correctly and that it handles errors properly.

But there’s a wrinkle. Android is the most test-hostile environment I’ve ever had the misfortune to find myself working in. I wonder sometimes if its designers deliberately designed it to make testing as difficult as they possibly could. It can be done, and I’ll show how in part 3, but if you’ll forgive me a digression, in this article I’m going to try the simple, “obvious” solution and demonstrate why it doesn’t work.

I’m going to try to write a simple test of an application that uses Android’s PowerManager service. PowerManager allows us to control when the device switches on or off. If we’re about to start some critical operation that must complete without the device switching off, we can obtain a WakeLock, which is what this sample app does.

The code is checked into GitHub. Here’s the code that we want to test:

package com.paulbutcher.powercontrol;

import android.app.Activity;
import android.os.Bundle;
import android.os.PowerManager;
import android.view.View;

public class PowerControl extends Activity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    }

    public void startImportant(View button) {
        PowerManager powerManager =
            (PowerManager)getSystemService(POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(
            PowerManager.FULL_WAKE_LOCK, "PowerControl");
        wakeLock.acquire();
    }

    public void stopImportant(View button) {
        wakeLock.release();
    }

    private PowerManager.WakeLock wakeLock;
}

Let’s try to write a test that verifies that startImportant calls PowerManager.newWakeLock. Our first task is going to be working out how to inject a mock PowerManager into the code under test.

That code obtains its PowerManager instance by calling Context.getSystemService. Happily, Android provides MockContext and ActivityUnitTestCase.setActivityContext, so we should be all set.

Before we get carried away, let’s just verify that we can get a test using a MockContext to run at all:

  def testAttempt1 {
    val mockContext = new MockContext;
    setActivityContext(mockContext)
    startActivity(startIntent, null, null)
  }

Let’s see what happens when we run that:

Failure in testAttempt1:
junit.framework.AssertionFailedError
  at android.test.ActivityUnitTestCase.startActivity(ActivityUnitTestCase.java:148)

Hmm—apparently this isn’t going to be as easy as we hoped.

The problem is that startActivity calls our MockContext. Specifically it calls getSystemService("layout_inflater") which fails because MockContext‘s methods are non-functional and throw UnsupportedOperationException.

It turns out that what Android means by “mock” isn’t what the rest of the world means. As Martin Fowler says in Mocks Aren’t Stubs, mocks are:

objects pre-programmed with expectations which form a specification of the calls they are expected to receive.

Never mind—there is another way. Android’s ContextWrapper allows us to wrap an existing context, only changing those bits of functionality we’re interested in for the purposes of our test:

  def testAttempt2 {
    val testContext =
        new ContextWrapper(getInstrumentation.getTargetContext);
    setActivityContext(testContext)
    startActivity(startIntent, null, null)
  }

That works, so now we just need to modify it to return a mock PowerManager when getSystemService is called:

  def testAttempt3 {
    val mockPowerManager = mock[PowerManager]
    val testContext =
      new ContextWrapper(getInstrumentation.getTargetContext) {
        override def getSystemService(name: String) = name match {
          case "power" => mockPowerManager
          case _ => super.getSystemService(name)
        }
      }
    setActivityContext(testContext)
    startActivity(startIntent, null, null)
  }

Which looks great, right up until we run it:

Error in testAttempt3:
java.lang.IllegalArgumentException: android.os.PowerManager is not an interface

Borachio, in common with most other mocking frameworks, can only mock interfaces, and PowerManager is a class, not an interface. There are mocking frameworks that can mock classes, for example PowerMock, but they rely on code generation, which Android’s Dalvik VM doesn’t (yet) support. So that’s not going to be any help :-(

There’s one final thing we can try. As well as mocking interfaces, Borachio can also mock functions. So we can derive from PowerManager and just mock the single method we’re interested in like this:

  def testAttempt4 {
    val mockNewWakeLock =
      mockFunction[Int, String, PowerManager#WakeLock]
    val mockPowerManager = new PowerManager {
      override def newWakeLock(flags: Int, tag: String) =
        mockNewWakeLock(flags, tag)
    }
    val testContext =
      new ContextWrapper(getInstrumentation.getTargetContext) {
        override def getSystemService(name: String) = name match {
          case "power" => mockPowerManager
          case _ => super.getSystemService(name)
        }
      }
    setActivityContext(testContext)
    startActivity(startIntent, null, null)
  }

But, when we compile this, we get:

PowerControlTest.scala:53: error: constructor PowerManager cannot be accessed in anonymous class $anon
    val mockPowerManager = new PowerManager {
                               ^

PowerManager‘s constructor is private :-(

And at this point, we’re out of easy options. We can’t play the same trick with PowerManager as we played with Context, as there’s no PowerManagerWrapper or similar.

We’re not beaten yet—I’ll show how to get around this problem in part 3 of this series. Small wonder, however, that most of the Android code I’ve seen has virtually no tests!

Get Involved

Get exclusive access to pre-release betas and talk to other SwiftKey fans.

VIP Community

@swiftkey