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)

Post a Comment