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
- Create a Android project project in Eclipse (File->New Project...->Android->Android Project)
- Name the project something like TDD Android
- Package name: com.example.tdd
- Activity name: TemperatureConverterActivity
- Application name: Temperature Converter
- 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.
- Select TDD Android project node in Packages
- Right click and select New Source Folder
- Folder name:test
- Finish
Then
- Select the newly created test folder
- Select New Java Package
- Name: com.example.tdd
- Finish
Generate test classes (JUnit version)
- Right click in the TemperatureConverter class
- Select New JUnit Test Case
- Select JUnit 3.x
- Source folder: TDD Android/test
- 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:
- add public void testCelsiusToFahrenheit()
- 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
- Add an attribute private static final Map<Integer, Integer> conversionTable
- Set its default value to new HashMap<Integer, Integer>()
- Shift + Ctrl + O to resolve the imports
Initialization code
- 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.
- 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));
}
}
- 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");
}
- 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
- Create the method public static int celsiusToFahrenheit(int celsius) in TemperatureConverter (Notice the int return type and the celsius parameter)
- 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:
- testCelsiusToFahrenheit failed because a wrong conversion result was obtained (remember that we returned a constant 0)
- 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.
- 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
- Source folder: TDD Android/test
- Package: com.example.tdd.validation
- Name: OverallTest
- Superclass: positron.TestCase
- Finish
Create Positron class
In the package com.example.tdd create the Positron class extending positron.Positron
- Source folder: TDD Android/test
- Package: com.example.tdd
- Name: Positron
- Superclass: positron.Positron
- 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)