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
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 checkpublic 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 folderCreate 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 thisadb 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 obtainI/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 runI/Positron(1430): Time: 2.408
I/Positron(1430): OK (4 tests)
25 comments:
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
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.
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
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?
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
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.
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
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.
this is awesome! but please keep update this page while the Positron has been a huge update. Thx for your great work
Thank for your comments.
Sure, I will be updating it to use new Positron library. Stay tuned.
I installe
another useful positron post here:
http://code.google.com/p/autoandroid/wiki/Positron
I have some problems when run "adb shell "/system/bin/am instrument -w com.example.tdd/.Positron"".
NoClassDefFoundError
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.
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
Opti Farms Keto supports healthy immunity and drives to reduce overweight size
of the body by eliminating bad cholesterol level. It improves ketosis that restricts
fat accumulation inside the body and makes best utilized as energy boost. The supplement
is one of the best selling product over the internet that makes you slim and stylish
Opti Farms Keto
AlkaTone Keto is a ketogenic Weight Loss supplement, and it has been formulated using different types of natural ingredients. If you intend to lose your weight permanently and if you want to spend your life happily and healthily. Then nothing can work better than this ketogenic weight loss formula.
AlkaTone Keto is a ketogenic Weight Loss supplement, and it has been formulated using different types of natural ingredients. If you intend to lose your weight permanently and if you want to spend your life happily and healthily. Then nothing can work better than this ketogenic weight loss formula.
Pure life keto pills are designed to help you reduce weight and improve focus without demanding a lot of effort which is generally required to lose weight. It’s a low carb diet, which works by using fats as the main source of energy instead of carbohydrates. It helps you to lose weight while maintaining and providing the energy boost you need to carry out all the activities you love doing. Kindly Visit on PureLife Keto Weight Loss Supplement
SwiftTrimKetoReviews It is a dietary supplement which enables the process of fat burn inside your body and allows your body to get rid of the extra accumulated fat inside it. The product helps in improving the metabolic system of your body. Also, the product helps in providing energy to your body. Visit On http://www.rushyourtrial.com/coupon/swift-trim-keto-real-energy-fat-burning-effects-trial-offer/
Ultra Keto Burn offers a dietary supplement which is supposed to assist in the wright loss process known as ketosis. When the body enters a state of metabolic ketosis, it is able to burn fat cells at a very quick pace. According to this product’s website, Ultra Keto Burn maximizes the efficiency of this diet. Visit On http://www.choosetolose.net/ultra-keto-burn-shark-tank-pills-review/
Alpha XTRM is a product which has multiple purposes. According to the manufacturer, it is supposed to help your muscles grow stronger and faster and help you get in good shape. It can apparently really enhance your sexual performance. The producer of this supplement claims that it radically increase your energy levels. Visit On http://www.theapexme.com/alpha-xtrm-risk-free-trials-male-enhancement-pills/
This is extremely great; it truly clarified TDD for me and why TDD is superior in terms ofbdd vs tdd vs ddd.
Post a Comment