http://www.developer.com/

Back to article

A Unit Testing Framework for the BlackBerry


July 24, 2008

The software development scene currently offers an overwhelming menu of programming languages and frameworks. That's a good thing—it means that people are still trying to find better ways to help us solve problems in code. It also means that there's always something fun and new to learn. The downside is that I'm continually debating which technologies to dig into more deeply. Potentially, I have the following courses that could end up on my plate: Ruby and Ruby on Rails, Groovy and Grails, Erlang, D, Scala, Objective C, Java 2 Micro Edition (J2ME). That's only scratching the surface. And so far, that's all I've done with most of these technologies—simply scratch the surface.

Which to choose, then? I firmly believe that mobile devices will be the predominant platform in the not-so-distant future. I decided to start building a mobile-centric entertainment product. My intent is to initially develop a BlackBerry CDLC (Connected Limited Device Configuration) J2ME client that interacts with a Java server. Going into this choice was my thought that the familiarity of Java would help me deliver an initial release more rapidly. My thought is that I'll then port it to the iPhone (hence my interest in re-investigating Objective C).

My initial experience with J2ME and BlackBerry programming seemed like a step backward. Forays into coding for the BlackBerry revealed that a number of things I took for granted about modern Java programming are no longer available: things like regular expressions, reflection, and a robust collection class library. Instead, I'm coding my own regex replace methods, figuring out how to come up with non-reflective but elegant solutions, and using the ancient standbys of Vector and Hashtable. Never mind modern Java conveniences like the enhanced-for loop, generics, enums, and annotations.

Most significantly, the lack of reflection and/or annotations means that a unit testing framework seemingly requires a bit more work on the part of the interested programmer. Version 3 and earlier of the JUnit framework take advantage of reflection to locate all of the test methods within a given test class. That means that a programmer doesn't have to explicitly list the test methods, a tedious and risky exercise. Version 4 of JUnit uses annotations to similarly simplify tagging test methods. J2ME has neither of these techniques available.

I searched for existing unit testing tools. I found mention of three: J2MEUnit, Mobile JUnit, and BUnit. Mobile JUnit uses a compile-time code generation scheme to supplant reflection. Because BlackBerry uses a custom VM, I'm pretty sure Mobile JUnit won't work for my needs. J2MEUnit's architectural choices and its use of the midlet framework didn't appeal to me, based on what I read. And BUnit requires the developer to explicitly list test methods.

Perhaps one of these test tools has some hidden, positive surprises, or perhaps there's another, better solution that I've not yet found. Shamefuly, I'm not patient enough to find out. On the basis of my BlackBerry programming naivete, I instead decided to build my own framework. I justified my decision as a great way to learn more about developing for the BlackBerry.

Cutting to the chase, Listing 1 shows what a sample test looks like.

Listing 1: A sample BBTest test.

import java.util.*;
import com.langrsoft.bbtest.*;

public class SampleTest extends MicroTestGroup {
   private Vector list;

   public SampleTest() {
      add(new MicroTest("passingUseOfAssertEquals") {
         public void run() {
            Assert.assertEquals(0, list.size());
         }
      });

      add(new MicroTest("failingExceptionExpectation") {
         public void run() {
            try {
               list.add(new Object());
               Assert.fail();
            }
            catch (IllegalArgumentException expected) {
            }
         }
      });

      add(new MicroTest("passing") {
         public void run() {
            Assert.assertTrue(list.isEmpty());
         }
      });
   }

   public void setUp() {
      list = new Vector();
   }

   public void tearDown() {
      list.clear();
   }
}

If you're not into anonymous inner classes, try one of the other tools! Using the BBTest framework, you create a test by deriving from the abstract class MicroTest and implementing the run method. You add each MicroTest subclass to a MicroTestGroup object. You name each test by passing a string to the constructor of MicroTest. MicroTestGroup objects can in turn hold other MicroTestGroup objects.

Other elements should look familiar. An Assert utility class provides a number of static assert methods, named after their JUnit analogs. MicroTestGroup also provides setUp and tearDown hooks for common initialization and clean-up, respectively.

I'll present interesting and core pieces of the framework in this article. You'll find a link to the complete BBTest framework at the end of this article. Note that it's a work in progress!

So, how does one build a test framework? Why, test-drive it, of course! In this case, I took an alternate approach: Instead of boot-strapping the framework with itself, I used JUnit 3.8, building the framework as a separate, normal Java project. The project contains a script that copies over the source to a BlackBerry JDE project.

First, the simple things. The Assert framework is straightforward. The most interesting aspect of test-driving the Assert framework is that AssertTest (see Listing 2) mixes both JUnit assertions and BBTest assertions. It's one of the few classes I've written where every class name is fully qualified! (Yes, there are less verbose solutions, but I wanted to avoid any possibility for confusion.)

Listing 2: AssertTest.

package com.langrsoft.bbtest;

import junit.framework.*;

public class AssertTest extends TestCase {
   public void testAssertTruePasses() {
      com.langrsoft.bbtest.Assert.assertTrue(true);
   }

   public void testAssertFalseFails() {
      try {
         com.langrsoft.bbtest.Assert.assertTrue(false);
         junitFail();
      } catch (com.langrsoft.bbtest.AssertionException
         expected) {
         junit.framework.Assert.assertEquals("",
            expected.getMessage());
      }
   }

   public void testAssertEqualsPassesWithTwoEqualValues() {
      com.langrsoft.bbtest.Assert.assertEquals(0, 0);
   }

   public void testAssertEqualsFailsWithTwoDifferentValues() {
      try {
         com.langrsoft.bbtest.Assert.assertEquals(0, 1);
         junitFail();
      } catch (com.langrsoft.bbtest.AssertionException
         expected) {
         junit.framework.Assert.assertEquals("expected <0>
            but was <1>", expected.getMessage());
      }
   }

   private void junitFail() {
      junit.framework.Assert.fail();
   }

   public void testAssertEqualsPassesWithTwoEqualReferences() {
      com.langrsoft.bbtest.Assert.assertEquals(new Integer(15),
         new Integer(15));
   }

   public void testAssertEqualsFailsWithTwoUnequalReferences() {
      try {
         com.langrsoft.bbtest.Assert.assertEquals(new Integer(15),
            new Integer(16));
         junitFail();
      } catch (com.langrsoft.bbtest.AssertionException expected) {
         junit.framework.Assert.assertEquals("expected <15>
            but was <16>", expected.getMessage());
      }
   }

   public void testAssertEqualsPassesWithTwoNullReferences() {
      com.langrsoft.bbtest.Assert.assertEquals(null, null);
   }

   public
      void testAssertEqualsFailsWithNullExpectedNonNullActual() {
      try {
         com.langrsoft.bbtest.Assert.assertEquals(null,
            new Integer(16));
         junitFail();
      } catch (com.langrsoft.bbtest.AssertionException
         expected) {
         junit.framework.Assert.assertEquals("expected <null>
            but was <16>", expected.getMessage());
      }
   }

   public void testFailFails() {
      try {
         com.langrsoft.bbtest.Assert.fail();
         junitFail();
      } catch (com.langrsoft.bbtest.AssertionException
         expected) {
         junit.framework.Assert.assertEquals("",
            expected.getMessage());
      }
   }
}

AssertTest is not at all complete. It provides only two overloaded versions of assertEquals—one for object references, one for int. I'll add the remainder as necessity calls. The assert methods in AssertTest do not provide "message strings"—strings to display when the test fails, mostly because I almost never use them (I can usually find a better way to make my tests more meaningful). I'll add them in the near future, if only to be more consistent with other frameworks.

Assertion failures are simple runtime exceptions of the type AssertionFailure, as Listing 3 shows.

Listing 3: Assert.

package com.langrsoft.bbtest;

public class Assert {
   public static void assertTrue(boolean actual) {
      if (!actual)
         throw new AssertionException("");
   }

   public static void assertEquals(int expected, int actual) {
      if (expected != actual)
         throwUnequal(String.valueOf(expected),
            String.valueOf(actual));
   }

   private static void throwUnequal(Object expected,
      Object actual) {
      throw new AssertionException("expected <" + expected +
         "> but was <" + actual + ">");
   }

   public static void assertEquals(Object expected, Object
      actual) {
      if ((expected == null && actual != null) ||
          (expected != null && !expected.equals(actual)))
         throwUnequal(expected, actual);
   }

   public static void fail() {
      throw new AssertionException("");
   }
}

Next, the MicroTest class, which represents a singular test to be executed. Listing 4 contains both MicroTest, its base class (which is also used by MicroTestGroup, presented later), and the interfaces it uses.

Listing 4: MicroTest.

// MicroTest.java:
package com.langrsoft.bbtest;

abstract public class MicroTest extends MicroTestBase implements
   Executable {
   public MicroTest() {
      super();
   }

   public MicroTest(String name) {
      super(name);
   }

   abstract public void run();

   public void execute() {
      passed = false;
      try {
         run();
         passed = true;
      } catch (Throwable failure) {
      }
   }

   public void execute(TestContext context) {
      try {
         context.setUp();
         execute();
         context.tearDown();
      } catch (Throwable failure) {
         passed = false;
      } finally {
         context.completed(this);
      )
   }
}

// MicroTestBase.java:
package com.langrsoft.bbtest;

public abstract class MicroTestBase implements Executable {
   protected boolean passed;
   protected String name;
   protected ResultListener listener;

   public MicroTestBase() {
      super();
   }

   public MicroTestBase(String name) {
      this.name = name;
   }

   public String name() {
      return name;
   }

   public boolean passed() {
      return passed;
   }

   abstract public void execute();

   public void setListener(ResultListener listener) {
      this.listener = listener;
   }

   public String className() {
      return simpleName(nestingClassName());
   }

   private String simpleName(String qualifiedName) {
      int start = qualifiedName.lastIndexOf('.');
      return qualifiedName.substring(start + 1);
   }

   private String nestingClassName() {
      String name = getClass().getName();
      int innerclassStart = name.indexOf('$');
      return (innerclassStart != -1) ? name.substring(0,
         innerclassStart) : name;
   }
)

// Executable.java:
package com.langrsoft.bbtest;

public interface Executable {
   public void execute();
   public void execute(TestContext context);
   public void setListener(ResultListener listener);
}

// ResultListener.java:
package com.langrsoft.bbtest;

public interface ResultListener {
   void ran(MicroTest test);
}

// TestContext.java:
package com.langrsoft.bbtest;

public interface TestContext {
   void setUp();
   void tearDown();
   void completed(MicroTest test);
}

The method simpleName in MicroTest demonstrates one of the frustrations of working with a more constrained library. The JDK 1.5 version of the class Class supplies the method getSimpleName; on the BlackBerry, I'm forced to hand-code a comparable method that suits my needs. Similarly, nestingClassName is a bit more work because a replace method that works with regular expressions is not available.

The meaning of the TestContext should be a bit clearer with the listing for MicroTestGroup (see Listing 5).

Listing 5: MicroTestGroup.

package com.langrsoft.bbtest;

import java.util.*;

public class MicroTestGroup extends MicroTestBase implements
   Executable, TestContext {
   private Vector tests = new Vector();

   public void add(Executable executable) {
      tests.addElement(executable);
   }

   public void execute() {
      for (Enumeration e = tests.elements();
         e.hasMoreElements();) {
         Executable executable = (MicroTestBase)e.nextElement();
         executable.setListener(listener);
         executable.execute(this);
      }
   }

   public void execute(TestContext context) {
      execute();
   }

   public void tearDown() {
   }

   public void setUp() {
   }

   public void completed(MicroTest test) {
      listener.ran(test);
   }}

The method execute with a TestContext argument allows the MicroTest to call back to the containing MicroTestGroup's setUp and tearDown methods.

The final listing that I'll present (Listing 6) provides a simple result listener implementation, which demonstrates how a client user interface can use the information coming back from a test run. I use this listener for the test for MicroTestGroup.

Listing 6: A Simple Result Listener Implementation

package com.langrsoft.bbtest;

import java.util.*;

public class SimpleTestCollector implements ResultListener {
   Vector allTests     = new Vector();
   Vector passingTests = new Vector();
   Vector failingTests = new Vector();

   public void ran(MicroTest test) {
      if (test.passed)
         passingTests.addElement(test);
      else
         failingTests.addElement(test);
      allTests.addElement(test);
   }

   public int passed() {
      return passingTests.size();
   }

   public int failed() {
      return failingTests.size();
   }

   public void assertPassFailCounts(int expectedPassed,
      int expectedFailed) {
      junit.framework.Assert.assertEquals(expectedPassed,
         passed());
      junit.framework.Assert.assertEquals(expectedFailed,
         failed());
   }
}

So far, learning how to program for the BlackBerry has been an interesting challenge, mostly due to the constrained Java environment. I've started building a user interface that I can deploy to the BlackBerry, so that I can actually use the BBTest framework.

To me, the development of this framework is another testament to the value of test-driven development (TDD). I have to admit that the first cut wasn't as clean as what you see here. But, because I had the high level of test coverage that TDD provides, I was able to incrementally refactor to a much more elegant solution.

Download the Code

You can download the source code here.

About the Author

Jeff Langr is a veteran software developer with over a quarter century of professional software development experience. He's written two books, including Agile Java: Crafting Code With Test-Driven Development (Prentice Hall) in 2005. Jeff has contributed a couple chapters to Uncle Bob Martin's upcoming book, Clean Code. Jeff has written over 75 articles on software development, with over thirty appearing at Developer.com. You can find out more about Jeff at his site, http://langrsoft.com, or you can contact him via email at jeff at langrsoft dot com.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date