Mock objects on Android with Borachio: Part 1

Now that Mockito has Android support, I am no longer supporting Borachio/ScalaMock on Android.

For a complete worked example of using Borachio on Android with RoboGuice for Dependency Injection, go here.

One of my biggest frustrations with writing code for Android has been the fact that none of the current Java mocking frameworks work on Android’s Dalvik VM. I recently released Borachio a native Scala mocking framework which does work on Android.

Because Borachio is written in Scala, you’ll need to write your tests in Scala. But it can be used to test code written in Java.

This post demonstrates how to get basic mocking working. Things get more complicated when you try to mock bits of Android itself, but I’ll cover that in a subsequent article.

This is an Android version of the example in Martin Fowler’s article Mocks Aren’t Stubs. The code is checked into GitHub here. You’ll need to have the Android SDK and Scala 2.8 installed to run this code.

We’re going to build a (very) simple ordering system. Orders will succeed if there’s enough inventory in our warehouse and fail if not. Let’s start by creating a very simple little Android application for us to test:

  1. Create a new project with:

    android create project -p WarehouseManager -t android-8 
        -p warehousemanager -k com.example.warehousemanager 
        -a WarehouseManager
  2. The core abstraction is a warehouse, represented by a Warehouse interface:

    package com.example.warehousemanager;
    
    public interface Warehouse {
        boolean hasInventory(String product, int quantity);
        void remove(String product, int quantity);
    }
  3. And here’s a very simple concrete implementation of Warehouse:

    package com.example.warehousemanager;
    
    import java.util.HashMap;
    
    public class RealWarehouse implements Warehouse {
        public RealWarehouse() {
            products = new HashMap();
            products.put("Talisker", 5);
            products.put("Lagavulin", 2);
        }
    
        public boolean hasInventory(String product, int quantity) {
            return inStock(product) >= quantity;
        }
    
        public void remove(String product, int quantity) {
            products.put(product, inStock(product) - quantity);
        }
    
        private int inStock(String product) {
            Integer quantity = products.get(product);
            return quantity == null ? 0 : quantity;
        }
    
        private HashMap products;
    }
  4. We remove things from the warehouse by placing an Order:

    package com.example.warehousemanager;
    
    public class Order {
    
        public Order(String product, int quantity) {
            this.product = product;
            this.quantity = quantity;
        }
    
        public void fill(Warehouse warehouse) {
            if (warehouse.hasInventory(product, quantity)) {
                warehouse.remove(product, quantity);
                filled = true;
            }
        }
    
        public boolean isFilled() {
            return filled;
        }
    
        private boolean filled = false;
        private String product;
        private int quantity;
    }
  5. We’ll need a UI to allow us to make orders, so modify main.xml to look like this:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        >
      <TextView  
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:text="Product:"
          />
      <EditText
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content"
          android:id="@+id/product"
          />
      <TextView
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content" 
          android:text="Quantity:"
          />
      <EditText
          android:layout_width="fill_parent" 
          android:layout_height="wrap_content"
          android:id="@+id/quantity"
          />
      <Button
          android:layout_height="wrap_content"
          android:layout_width="wrap_content"
          android:text="Place order"
          android:onClick="placeOrder" />
    </LinearLayout>
  6. And finally, here’s the implementation of WarehouseManager:

    package com.example.warehousemanager;
    
    import android.app.Activity;
    import android.os.Bundle;
    import android.view.View;
    import android.widget.EditText;
    import android.widget.Toast;
    
    public class WarehouseManager extends Activity
    {
        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
    
            productEditText = (EditText)findViewById(R.id.product);
            quantityEditText = (EditText)findViewById(R.id.quantity);
        }
    
        public void placeOrder(View view) {
            String product = productEditText.getText().toString();
            int quantity = Integer.parseInt(quantityEditText.getText().toString());
            Order order = new Order(product, quantity);
            order.fill(warehouse);
    
            String message = order.isFilled() ? "Success" : "Failure";
            Toast toast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
            toast.show();
        }
    
        private Warehouse warehouse = new RealWarehouse();
    
        private EditText productEditText;
        private EditText quantityEditText;
    }

You should now have a little Android application that can be compiled and installed with ant install. Here’s what it looks like:

Screenshot

So, now that we’ve got something to test, let’s create a test project to test it:

  1. Create a test project with:

    android create test-project -p test -m .. -n WarehouseManagerTest

    Next, we’ll convert this to a Scala project, as described here.

  2. Add scala.dir and proguard.dir to local.properties. Here’s what I added to mine (you’ll need to change the paths to match your local installation):

    scala.dir=/opt/local/share/scala-2.8
    proguard.dir=/Users/paul/android-sdk-mac_86/tools/proguard/
  3. Copy build-scala.xml into the root of the test project and add the following to build.xml:

    <import file="build-scala.xml" />
    
    <!-- Converts this project's .class files into .dex files -->
    <target name="-dex" depends="compile, scala-compile, scala-shrink">
        <scala-dex-helper />
    </target>
  4. Delete the proguard.cfg file and copy the configs directory into the test project. Add the following to the bottom of both default-debug.cfg and default-release.cfg (to ensure that ProGuard doesn’t discard our test classes:

    -keep public class * implements junit.framework.Test { public void test*(); }
  5. Copy the Borachio JAR to the libs directory.
  6. Finally, we can write our tests, which create mock instances of the Warehouse interface:

    package com.example.warehousemanager;
    
    import junit.framework.TestCase
    import com.borachio.junit3.MockFactory
    
    class OrderTest extends TestCase with MockFactory {
    
      def testInStock() {
        withExpectations {
          val mockWarehouse = mock[Warehouse]
          inSequence {
            mockWarehouse expects 'hasInventory withArguments ("Talisker", 50) returning true once;
            mockWarehouse expects 'remove withArguments ("Talisker", 50) once
          }
    
          val order = new Order("Talisker", 50)
          order.fill(mockWarehouse)
    
          assert(order.isFilled)
        }
      }
    
      def testOutOfStock() {
        withExpectations {
          val mockWarehouse = mock[Warehouse]
          mockWarehouse expects 'hasInventory returns false once
    
          val order = new Order("Talisker", 50)
          order.fill(mockWarehouse)
    
          assert(!order.isFilled)
        }
      }
    }
  7. Run the tests with:

    ant run-tests

In part 2, we’ll look at some of the challenges of mocking Android components.

Updated 2011-04-15

Updated to Borachio 0.6.

2 Responses to “Mock objects on Android with Borachio: Part 1”


  1. 1 Siddhu Warrier June 7, 2011 at 8:00 pm

    Hi Paul,

    I’m having a bit of trouble getting Borachio to play with me, and was wondering if you could help. I hope it isn’t a very stupid question as I hadn’t really played with Scala until earlier this evening.

    So I followed the instructions and configured my test project to work with Borachio. My test class itself contains nothing except the class def.

    When I run an ant debug to produce a test apk, I’m faced with this compiler error (partial stack trace from ant -v below):

    ************
    scala-compile:
    [property] Resource Loading compiler.properties
    [scalac] Scala version 2.8.1.final – http://scala-lang.org
    Property “scalac.addparams” has not been set
    [scalac] com/chipsaway/stages/login/GoogleAppsLoginStageTest.scala added as com/chipsaway/stages/login/GoogleAppsLoginStageTest.class doesn’t exist.
    [scalac] Compiling 1 source file to /Users/siddhu.warrier/Documents/workspace/ChipsAway/tests/bin/classes
    [scalac] No sources found.
    [scalac] No files selected for compilation
    [scalac] java.lang.AssertionError: assertion failed: adapt com.borachio.AbstractMockFactory$class.mockFunction(GoogleAppsLoginStageTest.this):()com.borachio.MockFunction0 to com.borachio.MockFunction0
    [scalac] at scala.tools.nsc.transform.Erasure$Eraser.adaptToType(Erasure.scala:533)
    [scalac] at scala.tools.nsc.transform.Erasure$Eraser.adapt(Erasure.scala:658)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:4208)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.transformedOrTyped(Typers.scala:4348)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.typedDefDef(Typers.scala:1789)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.typed1(Typers.scala:3862)
    [scalac] at scala.tools.nsc.transform.Erasure$Eraser.liftedTree1$1(Erasure.scala:663)
    [scalac] at scala.tools.nsc.transform.Erasure$Eraser.typed1(Erasure.scala:662)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:4203)
    [scalac] at scala.tools.nsc.typechecker.Typers$Typer.typed(Typers.scala:4252)

    ************

    I’d be really grateful if you could help me out with this as it would save me so much hassle on two projects I’m working on! :)

    Thanks in advance!

  2. 2 paul June 8, 2011 at 2:47 pm

    Lest anyone else experiences the same problem as Siddhu above, it turned out to be using the version of Borachio compiled with Scala 2.9.0 in a Scala 2.8.1 project.


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





Follow

Get every new post delivered to your Inbox.

Join 242 other followers

%d bloggers like this: