9 min read

 

Android Application Testing Guide

Android Application Testing GuideBuild intensively tested and bug free Android applications 

The user interface is in place. Now we start adding some basic functionality.

This functionality will include the code to handle the actual temperature conversion.

Temperature conversion

From the list of requirements from the previous article we can obtain this statement: When one temperature is entered in one field the other one is automatically updated with the conversion.

Following our plan we must implement this as a test to verify that the correct functionality is there. Our test would look something like this:

@UiThreadTest
public final void testFahrenheitToCelsiusConversion() {
mCelsius.clear();
mFahrenheit.clear();
final double f = 32.5;
mFahrenheit.requestFocus();
mFahrenheit.setNumber(f);
mCelsius.requestFocus();
final double expectedC =
TemperatureConverter.fahrenheitToCelsius(f);
final double actualC = mCelsius.getNumber();
final double delta = Math.abs(expectedC – actualC);
final String msg = “” + f + “F -> ” + expectedC + “C
but was ” + actualC + “C (delta ” + delta + “)”;
assertTrue(msg, delta < 0.005);
}


Firstly, as we already know, to interact with the UI changing its values we should run the test on the UI thread and thus is annotated with @UiThreadTest.

Secondly, we are using a specialized class to replace EditText providing some convenience methods like clear() or setNumber(). This would improve our application design.

Next, we invoke a converter, named TemperatureConverter, a utility class providing the different methods to convert between different temperature units and using different types for the temperature values.

Finally, as we will be truncating the results to provide them in a suitable format presented in the user interface we should compare against a delta to assert the value of the conversion.

Creating the test as it is will force us to follow the planned path. Our first objective is to add the needed code to get the test to compile and then to satisfy the test’s needs.

The EditNumber class

In our main project, not in the tests one, we should create the class EditNumber extending EditText as we need to extend its functionality.

We use Eclipse’s help to create this class using File | New | Class or its shortcut in the Toolbars.

This screenshot shows the window that appears after using this shortcut:

Android Application Testing: Adding Functionality to the UI

The following table describes the most important fields and their meaning in the previous screen:

 

 

Field

Description

Source folder:

The source folder for the newly-created class. In this case the default location is fine.

Package:

The package where the new class is created. In this case the default package com.example.aatg.tc is fine too.

Name:

The name of the class. In this case we use EditNumber.

Modifiers:

Modifiers for the class. In this particular case we are creating a public class.

Superclass:

The superclass for the newly-created type. We are creating a custom View and extending the behavior of EditText, so this is precisely the class we select for the supertype.

Remember to use Browse… to find the correct package.

Which method stubs would you like to create?

These are the method stubs we want Eclipse to create for us. Selecting Constructors from superclass and Inherited abstract methods would be of great help.

As we are creating a custom View we should provide the constructors that are used in different situations, for example when the custom View is used inside an XML layout.

Do you want to add comments?

Some comments are added automatically when this option is selected.

You can configure Eclipse to personalize these comments.

Once the class is created we need to change the type of the fields first in our test:

public class TemperatureConverterActivityTests extends
ActivityInstrumentationTestCase2<TemperatureConverterActivity> {
private TemperatureConverterActivity mActivity;
private EditNumber mCelsius;
private EditNumber mFahrenheit;
private TextView mCelsiusLabel;
private TextView mFahrenheitLabel;


Then change any cast that is present in the tests. Eclipse will help you do that.

If everything goes well, there are still two problems we need to fix before being able to compile the test:

  • We still don’t have the methods clear() and setNumber() in EditNumber
  • We don’t have the TemperatureConverter utility class

To create the methods we are using Eclipse’s helpful actions. Let’s choose Create method clear() in type EditNumber.

Same for setNumber() and getNumber().

Finally, we must create the TemperatureConverter class.

Be sure to create it in the main project and not in the test project.

Android Application Testing: Adding Functionality to the UI

Having done this, in our test select Create method fahrenheitToCelsius in type TemperatureConverter.

This fixes our last problem and leads us to a test that we can now compile and run.

Surprisingly, or not, when we run the tests, they will fail with an exception:

09-06 13:22:36.927: INFO/TestRunner(348): java.lang.
ClassCastException: android.widget.EditText
09-06 13:22:36.927: INFO/TestRunner(348): at com.example.aatg.
tc.test.TemperatureConverterActivityTests.setUp(
TemperatureConverterActivityTests.java:41)
09-06 13:22:36.927: INFO/TestRunner(348): at junit.framework.
TestCase.runBare(TestCase.java:125)


That is because we updated all of our Java files to include our newly-created EditNumber class but forgot to change the XMLs, and this could only be detected at runtime.

Let’s proceed to update our UI definition:

<com.example.aatg.tc.EditNumber
android_layout_height=”wrap_content”
android_id=”@+id/celsius”
android_layout_width=”match_parent”
android_layout_margin=”@dimen/margin”
android_gravity=”right|center_vertical”

android_saveEnabled=”true” />


That is, we replace the original EditText by com.example.aatg.tc.EditNumber which is a View extending the original EditText.

Now we run the tests again and we discover that all tests pass.

But wait a minute, we haven’t implemented any conversion or any handling of values in the new EditNumber class and all tests passed with no problem. Yes, they passed because we don’t have enough restrictions in our system and the ones in place simply cancel themselves.

Before going further, let’s analyze what just happened. Our test invoked the mFahrenheit.setNumber(f) method to set the temperature entered in the Fahrenheit field, but setNumber() is not implemented and it is an empty method as generated by Eclipse and does nothing at all. So the field remains empty.

Next, the value for expectedC—the expected temperature in Celsius is calculated invoking TemperatureConverter.fahrenheitToCelsius(f), but this is also an empty method as generated by Eclipse. In this case, because Eclipse knows about the return type it returns a constant 0. So expectedC becomes 0.

Then the actual value for the conversion is obtained from the UI. In this case invoking getNumber() from EditNumber. But once again this method was automatically generated by Eclipse and to satisfy the restriction imposed by its signature, it must return a value that Eclipse fills with 0.

The delta value is again 0, as calculated by Math.abs(expectedC – actualC).

And finally our assertion assertTrue(msg, delta < 0.005) is true because delta=0 satisfies the condition, and the test passes.

So, is our methodology flawed as it cannot detect a simple situation like this?

No, not at all. The problem here is that we don’t have enough restrictions and they are satisfied by the default values used by Eclipse to complete auto-generated methods. One alternative could be to throw exceptions at all of the auto-generated methods, something like RuntimeException(“not yet implemented”) to detect its use when not implemented. But we will be adding enough restrictions in our system to easily trap this condition.

TemperatureConverter unit tests

It seems, from our previous experience, that the default conversion implemented by Eclipse always returns 0, so we need something more robust. Otherwise this will be only returning a valid result when the parameter takes the value of 32F.

The TemperatureConverter is a utility class not related with the Android infrastructure, so a standard unit test will be enough to test it.

We create our tests using Eclipse’s File | New | JUnit Test Case, filling in some appropriate values, and selecting the method to generate a test as shown in the next screenshot.

Firstly, we create the unit test by extending junit.framework.TestCase and selecting com.example.aatg.tc.TemperatureConverter as the class under test:

Android Application Testing: Adding Functionality to the UI

Then by pressing the Next > button we can obtain the list of methods we may want to test:

Android Application Testing: Adding Functionality to the UI

We have implemented only one method in TemperatureConverter, so it’s the only one appearing in the list. Other classes implementing more methods will display all the options here.

It’s good to note that even if the test method is auto-generated by Eclipse it won’t pass. It will fail with the message Not yet implemented to remind us that something is missing.

Let’s start by changing this:

/**
* Test method for {@link com.example.aatg.tc.
TemperatureConverter#fahrenheitToCelsius(double)}.
*/
public final void testFahrenheitToCelsius() {
for (double c: conversionTableDouble.keySet()) {
final double f = conversionTableDouble.get(c);
final double ca = TemperatureConverter.fahrenheitToCelsius(f);
final double delta = Math.abs(ca – c);
final String msg = “” + f + “F -> ” + c + “C but is ”
+ ca + ” (delta ” + delta + “)”;
assertTrue(msg, delta < 0.0001);
}
}


Creating a conversion table with values for different temperature conversion we know from other sources would be a good way to drive this test.

private static final HashMap<Double, Double>
conversionTableDouble = new HashMap<Double, Double>();
static {
// initialize (c, f) pairs
conversionTableDouble.put(0.0, 32.0);
conversionTableDouble.put(100.0, 212.0);
conversionTableDouble.put(-1.0, 30.20);
conversionTableDouble.put(-100.0, -148.0);
conversionTableDouble.put(32.0, 89.60);
conversionTableDouble.put(-40.0, -40.0);
conversionTableDouble.put(-273.0, -459.40);
}


We may just run this test to verify that it fails, giving us this trace:

junit.framework.AssertionFailedError: -40.0F -> -40.0C but is 0.0
(delta 40.0)at com.example.aatg.tc.test.TemperatureConverterTests.
testFahrenheitToCelsius(TemperatureConverterTests.java:62)
at java.lang.reflect.Method.invokeNative(Native Method)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.
java:169)
at android.test.AndroidTestRunner.runTest(AndroidTestRunner.
java:154)
at android.test.InstrumentationTestRunner.onStart(
InstrumentationTestRunner.java:520)
at android.app.Instrumentation$InstrumentationThread.run(
Instrumentation.java:1447)


Well, this was something we were expecting as our conversion always returns 0. Implementing our conversion, we discover that we need some ABSOLUTE_ZERO_F constant:

public class TemperatureConverter {
public static final double ABSOLUTE_ZERO_C = -273.15d;
public static final double ABSOLUTE_ZERO_F = -459.67d;
private static final String ERROR_MESSAGE_BELOW_ZERO_FMT =
“Invalid temperature: %.2f%c below absolute zero”;
public static double fahrenheitToCelsius(double f) {
if (f < ABSOLUTE_ZERO_F) {
throw new InvalidTemperatureException(
String.format(ERROR_MESSAGE_BELOW_ZERO_FMT, f, ‘F’));
}
return ((f – 32) / 1.8d);
}
}


Absolute zero is the theoretical temperature at which entropy would reach its minimum value. To be able to reach this absolute zero state, according to the laws of thermodynamics, the system should be isolated from the rest of the universe. Thus it is an unreachable state. However, by international agreement, absolute zero is defined as 0K on the Kelvin scale and as -273.15°C on the Celsius scale or to -459.67°F on the Fahrenheit scale.

We are creating a custom exception, InvalidTemperatureException, to indicate a failure providing a valid temperature to the conversion method. This exception is created simply by extending RuntimeException:

public class InvalidTemperatureException extends RuntimeException {
public InvalidTemperatureException(String msg) {
super(msg);
}
}


Running the tests again we now discover that testFahrenheitToCelsiusConversion test fails, however testFahrenheitToCelsius succeeds. This tells us that now conversions are correctly handled by the converter class but there are still some problems with the UI handling this conversion.

A closer look at the failure trace reveals that there’s something still returning 0 when it shouldn’t.

This reminds us that we are still lacking a proper EditNumber implementation. Before proceeding to implement the mentioned methods, let’s create the corresponding tests to verify what we are implementing is correct.

LEAVE A REPLY

Please enter your comment!
Please enter your name here