Wednesday, January 23, 2008

Test Driven Development and GUI Testing on the Android platform: Temperature Converter sample

Google Docs: This article can be viewed at Test Driven Development and GUI Testing on Android platform: Temperature Converter sample

Unit tests

This article will introduce Test Driven Development on the Google Android platform. I took me some time to figure it all out and it's not always clear where to find the correct documentation, answers or resources, so I'm writing this hoping it be helpful for someone who is trying to take this approach.

This is an open topic, an we are all doing the first steps and trying to discover some functionality which is still not documented in the Android SDK, and up to now there's no sources to check what's the intention of some API code or tools.

We will be using a fairly simple example that was used in previous articles about similar subjects. That's a Temperature Converter application. Requirements are defined by our Use Case: Convert Temperature.

Using Test Driven Development we will implement this use case, trying to keep it as simple as possible not to get confused with unnecessary details.


Comments, corrections, improvements, critics are gladly welcome.

Project

  1. Create a Android project project in Eclipse (File->New Project...->Android->Android Project)
  2. Name the project something like TDD Android
  3. Package name: com.example.tdd
  4. Activity name: TemperatureConverterActivity
  5. Application name: Temperature Converter
  6. Finish


Add testing infrastructure using JUnit

We are using a different folder to keep our tests in order to permit a clear separation in case the test should be removed at production.

  1. Select TDD Android project node in Packages
  2. Right click and select New Source Folder
  3. Folder name:test
  4. Finish

Then

  1. Select the newly created test folder
  2. Select New Java Package
  3. Name: com.example.tdd
  4. Finish


Generate test classes (JUnit version)


  1. Right click in the TemperatureConverter class
  2. Select New JUnit Test Case
  3. Select JUnit 3.x
  4. Source folder: TDD Android/test
  5. Finish

Now we will add our tests, even though our TemperatureConverter class is not clearly defined. Adding the tests will help us define our TemperatureConverter class.

Define the tests

Accordingly with our use case in Temperature Converter we can define 2 tests

  • testCelsiusToFahrenheit
  • testInvalidCelsiusTemperature

to verify our tests' resluts we will use some data obtained from an external converter and we will put these figures in a conversion table. Some temperature measure background can be obtained from http://en.wikipedia.org/wiki/Temperature and an online converter can be found at http://www.onlineconversion.com/temperature.htm. We will add a conversionTable to verify our tests shortly.

Edit the source file and:

  1. add public void testCelsiusToFahrenheit()
  2. add public void testInvalidCelsiusTemperature()


And use this code snippet

 public void testCelsiusToFahrenheit() {
fail("Not implemented yet");
}

public void testInvalidCelsiusTemperature() {
fail("Not implemented yet");

Adding the conversion table

Also in the TemperatureConversionTest class

  1. Add an attribute private static final Map<Integer, Integer> conversionTable
  2. Set its default value to new HashMap<Integer, Integer>()
  3. Shift + Ctrl + O to resolve the imports

Initialization code

  1. Initialize the conversion table to
static { // initialize (c, f) pairs conversionTable.put(0, 32); conversionTable.put(100, 212); conversionTable.put(-1, 30); conversionTable.put(-100, -148); conversionTable.put(32, 90); conversionTable.put(-40, -40); conversionTable.put(-273, -459); }

just after its definition

Run the tests

Right click on the TDD Android project node and select Run as JUnit Test We will se how, as expected. our tests fail.

Notice that these tests will run outside the Android emulator. Later we will include these tests to be run by the emulator itself.

Complete the tests


Now, once our test infrastructure is setup, let's proceed to define our tests.

  1. Add
public void testCelsiusToFahrenheit () 
for (int c: conversionTable.keySet()) {
int f = conversionTable.get(c);
String msg = "" + c + "C -> " + f + "F";
assertEquals(msg, f, TemperatureConverter.celsiusToFahrenheit(c));
}
}
  1. Add
   public void invalidCelsiusTemperature () {
try {
int f = TemperatureConverter.celsiusToFahrenheit(-274);
} catch (RuntimeException ex) {
if (ex.getMessage().contains("below absolute zero")) {
return;
}
else {
fail("Undetected temperature below absolute zero: " + ex.getMessage());
}

}

fail("Undetected temperature below absolute zero: no exception generated");

}
  1. If some imports are missing just press Shift + Ctrl + O

Creating the real code

We have now the tests for code that still doesn't exist. If everything was fine, you should see a light bulb in the line where TemperatureConverter.celsiusToFahrenheit is invoked and giving you the alternative to

Create Method celsiusToFarenheit(int) in tdd.TemperatureConverter 
  1. Create the method public static int celsiusToFahrenheit(int celsius) in TemperatureConverter (Notice the int return type and the celsius parameter)
  2. Return any value, say 0, or raise an exception indicating that it's not implemented yet.

Running the tests

Our Test Driven Development is starting to appear.

Go to the JUnit tab and Rerun Tests

These tests will fail, because we haven't already implemented the celsiusToFahrenheit conversion method.

We obtain two failures but by very different reasons as before:

  1. testCelsiusToFahrenheit failed because a wrong conversion result was obtained (remember that we returned a constant 0)
  2. testInvalidCelsiusTemperature failed because no exception was generated for an invalid temperature

So, what's left is to go there an implement it.

celsiusToFahrenheit

Replace this

return 0

by

return (int)Math.round(celsius * 1.8 + 32);

Remember to change parameters name to celsius (from c) if it's not already named.

Running the tests again, and we will see how one test succeed but the other fails with the message

Undetected temperature below absolute zero: no exception generated

That's because we are expecting an exception to be thrown if the temperature is below the absolute zero but our first attempt doesn't do that.

Let's add this condition, but we first need the ABSOLUTE_ZERO_C constant.

Add ABSOLUTE_ZERO_C constant

TemperatureConverter class editor window.

  1. Add
    public static final int ABSOLUTE_ZERO_C = -273;

Modify celsiusToFahrenheit

Add the absolute zero check
  public static int celsiusToFahrenheit(int celsius) {
if (celsius < ABSOLUTE_ZERO_C) {
throw new RuntimeException("Invalid temperature: " + celsius + " below absolute zero");
}
return (int)Math.round(celsius * 1.8 + 32);

}

Run the tests

Run the tests again and we can verify that in our third attempt both tests passed.

Functional tests

We now have our TemperatureConverter class tested and ready to be included in our Android project.

Create the layout

Select main.xml in layout and add
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout id="@+id/linear" android:layout_width="fill_parent"
android:layout_height="fill_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical">
<TextView id="@+id/message"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="6dip"
android:text="Enter the temperature and press Convert">
</TextView>
<TableLayout id="@+id/table" android:layout_width="fill_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:stretchColumns="1">
<TableRow>
<TextView id="@+id/celsius_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="3dip"
android:textStyle="bold"
android:textAlign="end"
android:text="Celsius">
</TextView>
<EditText id="@+id/celsius"
android:padding="3dip"
android:numeric="true"
android:digits="-+0123456789"
android:textAlign="end"
android:singleLine="true"
android:scrollHorizontally="true"
android:nextFocusDown="@+id/convert"
>
</EditText>
</TableRow>
<TableRow>
<TextView id="@+id/fahrenheit_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="3dip"
android:textStyle="bold"
android:textAlign="end"
android:text="Fahrenheit">
</TextView>
<EditText id="@+id/fahrenheit"
android:padding="3dip"
android:textAlign="end"
android:singleLine="true"
android:scrollHorizontally="true">
</EditText>
</TableRow>
</TableLayout>
<Button id="@+id/convert" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:padding="6dip"
android:text="Convert"
android:textAlign="center"
android:layout_gravity="center_horizontal"
android:nextFocusUp="@+id/celsius"
>
</Button>
</LinearLayout>

There's a lot of documentation and tutorials online in case you need to know more about Android layouts. We now have an empty form.

Change the theme, if you want, in AndroidManifest.xml, adding

android:theme="@android:style/Theme.Dark"

to <application>

We will obtain this when we run as Android Application

Of course this is not functional yet.

Positron

provides an instrumentation and some support classes to help writing acceptance tests. It is provided as a jar that gets bundled with your application. Acceptance tests are written in junit, extending a custom base class. Positron can be downloaded from http://code.google.com/p/android-positron/downloads/list.

Positron jar must be added to build path. In this example we are using positron-0.4-alpha, but if by the time you read this there's a newer version you may use it.

Validation

Create com.example.tdd.validation package in test folder

Create OverallTest test case

In the newly created package com.example.tdd.validation create a new JUnit test case

  1. Source folder: TDD Android/test
  2. Package: com.example.tdd.validation
  3. Name: OverallTest
  4. Superclass: positron.TestCase
  5. Finish

Create Positron class

In the package com.example.tdd create the Positron class extending positron.Positron

  1. Source folder: TDD Android/test
  2. Package: com.example.tdd
  3. Name: Positron
  4. Superclass: positron.Positron
  5. Finish

Then, create our test suite including OverallTest tests. Other tests can also be added.

 @Override
protected TestSuite suite() {
TestSuite suite = new TestSuite();
suite.addTestSuite(OverallTest.class);
return suite;
}

Create the test


In OverallTest create the testConversion test case

 public void testConversion() {
Intent intent = new Intent(getTargetContext(), TemperatureConverterActivity.class);
startActivity(intent.addLaunchFlags(Intent.NEW_TASK_LAUNCH));

TemperatureConverterActivity activity = (TemperatureConverterActivity)activity();

// Is it our application ?
assertEquals(getTargetContext().getString(R.string.app_name), activity().getTitle());

// Do we have focus ?
assertEquals(activity.getCelsiusEditText(), activity.getCurrentFocus());

// Enter a temperature
sendString("123");

// Convert
press(DOWN, CENTER);

// Verify correct conversion 123C -> 253F
assertEquals("253", activity.getFahrenheitEditText().getText().toString());

}

This basically represents what we defines in our Use Case: Convert Temperature:

Actor Action System Response
1. The user enters a temperature in Celsius, and then press Convert 2. The system converts the temperature to Fahrenheit and the result is presented to the user
3. The user wants to enter another temperature and continues from 1 or presses Back to exit. 4. The process finishes.
Alternative Courses
Temperature is below absolute zero Indicate error
Invalid input characters entered Indicate error

Finally, add this to tearDown

 protected void tearDown() throws Exception {
finishAll();
}

Instrumentation

Add the instrumentation definition to AndroidManifest.xml
<instrumentation class=".Positron" android:functionalTest="true" android:targetPackage="com.example.tdd" android:label="Temperature Converter Acceptance Tests"/>


Complete TemperatureConverterActivity

In order to compile these tests we have to add the fileds related with the views widget in the GUI
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setContentView(R.layout.main);

// Find text fields.
celsiusEditText = (EditText)findViewById(R.id.celsius);
fahrenheitEditText = (EditText)findViewById(R.id.fahrenheit);
convertButton = (Button)findViewById(R.id.convert);

// disable fahrenheit field
fahrenheitEditText.setEnabled(false);

}

Also add the fields proposed by the IDE

private EditText celsiusEditText;;
private EditText fahrenheitEditText;
private Button convertButton;

and using Source->Generate Getters and Setters... generate the corresponding getters.

Run the tests again

This time let's run the positron tests in the emulator. To achieve this

adb shell "/system/bin/am instrument -w com.example.tdd/.Positron"

and change to the DDMS inside Eclipse, and look for the LogCat window

I/Positron(1091): .F
I/Positron(1091): Time: 1.332
I/Positron(1091): There was 1 failure:
I/Positron(1091): 1) testConversion(com.example.tdd.validation.OverallTest)junit.framework.ComparisonFailure: expected:<253> but was:<>
I/Positron(1091): at com.example.tdd.validation.OverallTest.testConversion(OverallTest.java:62)
I/Positron(1091): at java.lang.reflect.Method.invokeNative(Native Method)
I/Positron(1091): at positron.harness.InstrumentedTestResult.run(InstrumentedTestResult.java:37)
I/Positron(1091): at junit.extensions.TestDecorator.basicRun(TestDecorator.java:22)
I/Positron(1091): at junit.extensions.TestSetup$1.protect(TestSetup.java:19)
I/Positron(1091): at junit.extensions.TestSetup.run(TestSetup.java:23)
I/Positron(1091): at positron.Positron.onStart(Positron.java:62)
I/Positron(1091): at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1115)
I/Positron(1091): FAILURES!!!
I/Positron(1091): Tests run: 1, Failures: 1, Errors: 0

Again, as expected the test failed because the conversion functionality is not yet implemented.

Implementing conversion functionality in TemperatureConverterActivity

Add OnClickListener to convert button


It's an interface definition for a callback to be invoked when a view, a button in this case, is clicked. We need to implement the onClick abstract method, which in our case will call the convert helper method to carry away the actual conversion.

        // Hook up button presses to the appropriate event handler.
convertButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View arg0) {
convert();
}
});

Add convert method

Helper method to get the temperature value entered into the Celsius text field, convert it into Integer and call celsiusToFahrenheit method in TemperatureConverter class.

   protected void convert() {
try {
int f = TemperatureConverter.celsiusToFahrenheit(Integer.parseInt(celsiusEditText.getText().toString()));
fahrenheitEditText.setText(String.valueOf(f));
}
catch (Exception ex) {
fahrenheitEditText.setText(ex.toString());
}

}

Run tests again

Now, when we run our tests again, we obtain
I/Positron(1334): Time: 1.368
I/Positron(1334): OK (1 test)

Let's convert our unit test into Positron

In the previous unit test TemperatureConverterTest let's change the base class into positron.TestCase. You may also have to comment out the generated constructor because (until now) there's no positron.TestCase constructor receiving a String.

Run tests again

Running the tests again now we can see all of the tests are run
I/Positron(1430): Time: 2.408
I/Positron(1430): OK (4 tests)

15 comments:

wonglik said...

Hi. I am not an expert on TDD so maybe I am just wrong but why run test on emulator? It is just time consuming? Are we testing something by this? Also as far as I remember from some interview with Martin folder we should concentrate on some small functionality so not sure if it is good idea to start writing both invalidCelsiusTemperature() and testCelsiusToFahrenheit () at the same time.
But any way article is nice and I think it is good idea to try to develop android software with TDD. Regards

Diego Torres Milano said...

Thanks for your comments.
You must run tests on the emulator because this is the target platform. Some tests could have different results on the emulator (and the phone) than in the development hosts. I can remember some differences parsing XML files and for sure others may exist.

You are completely right on your comment, you should concentrate on small functionality, so in a real case not so obvious as this trivial example you should implement those tests in different iterations.

wonglik said...

Ok. Probably I am just more used to develop J2EE apps where the gap between development env and production is smaller then in J2ME or Android.

Hope to read more news on Your adventures on TDD and Android :)

regards

Rakoun.com said...

Hi, thanks for your tuto.
Now I would like to know how can I introduce SQLite in my Unit Tests. I have several domain classes wich are mapped to SQLite Database tables. In my android project it is easy to deal with SQLite since my application inherits from Activiy (I have the context). But in my mapped test classes I do not have the context and I don't know how to deal with it? Must I to install a instance of SQLite on my computer to test my domain classes?

Any idea?

gymshoe said...

Great post, Thanks.
I am fairly new to android, and trying to use TDD (Junit 3) on android-0.9SDK, and Eclipse 3.4.0. I cannot reproduce your results because even with minimal code in my test case (class declaration only), it immediately fails with the following error:
"Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/ref/FinalReference "


Any ideas? Have you been able to use 0.9 with TDD successfully?

thanks,
Jim

Diego Torres Milano said...

Thanks for your comments.
I'm slowly migrating everything to 0.9 aiming to 1.0, so stay tuned and expect an update on most of the post in the next few weeks, but still using Eclipse 3.3 Europa.
With respect to your comment about writing two test at the same time, you are absolutely right, in a real scenario these two tests should be added in two different iterations.
Regards.

Stb said...

nice tutorial.

just had a problem setting up junit for android projects(the tests would not run at all within the Eclipse JUnit UI.

Found the solution here
http://code.google.com/android/kb/troubleshooting.html#addjunit
hope this helps
cheers
stb

Diego Torres Milano said...

Thanks for your comments and link.
This way you can run tests inside Eclipse but don't forget that more of the tests are valid only running on the device or the emulator.

LukeW said...

this is awesome! but please keep update this page while the Positron has been a huge update. Thx for your great work

Diego Torres Milano said...

Thank for your comments.
Sure, I will be updating it to use new Positron library. Stay tuned.

prassa said...

I installe

monkeywoo said...

another useful positron post here:
http://code.google.com/p/autoandroid/wiki/Positron

Linhai said...

I have some problems when run "adb shell "/system/bin/am instrument -w com.example.tdd/.Positron"".


NoClassDefFoundError

Diego Torres Milano said...

Use this syntax:


$ adb shell am instrument -w com.example.myfirstproject.test/android.test.InstrumentationTestRunner

to run all tests in the com.example.myfirstproject.test package.

Android app development said...

This is one of the good post.I like your blog status.i like your Information.Nicely you describe this blog.good post.Android app developers