Today I would like the series of articles about Android Testing. I’m planning create articles about different type of testing (Unit testing, UI testing) and categories of Android test (local unit tests, instrumentation tests) and different frameworks (JUnit, Espresso, Robolectric). How to configure android project for different type of testing and how to run different type of testing.
Testing is the process of checking the functionality of the application whether it is working as per requirements and to ensure that at developer level unit testing comes into picture. Unit testing is the testing of single entity (class or method). Unit testing is very essential to every software company to give a quality product to their customers.
JUnit is a unit testing framework for the Java programming language. JUnit has been important in the development of test-driven development.
Unit testing can be done in two different ways:
- Manual Testing:
- Time consuming and tedious: Since test cases are executed by human resources so it is very slow and tedious;
- Huge investment in human resources: As test cases need to be executed manually so more testers are required in manual testing;
- Less reliable: Manual testing is less reliable as tests may not be performed with precision each time because of human errors;
- Non-programmable: No programming can be done to write sophisticated tests which fetch hidden information.
- Automated testing:
- Fast Automation runs test cases significantly faster than human resources;
- Less investment in human resources: Test cases are executed by using automation tool so less tester are required in automation testing;
- More reliable: Automation tests perform precisely same operation each time they are run;
- Programmable: Testers can program sophisticated tests to bring out hidden information.
I’m talking about use JUnit 4. In this article I show how to test business logic of simple application. This application allow to calculate sum of ordered coffee.
First of all you must to know difference between local unit tests and instrumentation tests.
The local unit tests run on local JVM (Java virtual machine). Instrumental test, in it’s turn, required Android device. Unfortunately for some tests you must use Android device. In this article I’m talking about local unit tests.
Structure of project:
- Source code:
{MODULE}/src/main/java
- Unit tests (which can run on local JVM):
{MODULE}/src/test/java
- Instrumentation tests (which should run on an Android device):
{MODULE}/src/androidTest/java
JUnit framework has set of basic assets:
void assertEquals(expected, actual)
- check that two primitives/Objects are equal;void assertTrue(condition)
- check that a condition is true;void assertFalse(condition)
- check that a condition is false;void assertNotNull(object)
- check that an object isn't null;void assertNull(object)
- check that an object is null;void assertSame(expected, actual)
- theassertSame()
methods tests if two object references point to the same object;void assertNotSame(unexpected, actual)
- theassertNotSame()
methods tests if two object references not point to the same object;void assertArrayEquals(expectedArray, actualArray)
- theassertArrayEquals()
method will test whether two arrays are equal to each other.
Right now I want to show simple example of using assertEquals
. As example, some class in the application has a method which allow summarize two number.
public int plus(int firstValue, int secondValue) {
return firstValue + secondValue;
}
We can create a test for this method.
@Test
public void plusOperationTest() {
assertEquals(4, plus(2, 2));
}
As you can see assertEquals
method has two parameters. The first parameter has expected value and the second parameter has actual value from method which summarize two number values. As you can see in this test we can check that if we put 2 and 2 to plus method, we are expecting to get result 4.
This is really simple test.
I would like to tell you about some annotation, also. Annotations are like meta-tags that you can add to your code and apply them to methods, variables, classes.
@Test
The @Test
annotation tells JUnit that the public void method to which it is attached can be run as a test case.
@Before
Several tests need similar objects created before they can run. Annotating a public void method with @Before
let’s JUnit that this method to be run before each Test method.
@After
If you allocate some external resources in a @Before
method you need to release them after the test runs. Annotating a public void method with @After
allows to do it.
@BeforeClass
Annotating a public static void method with @BeforeClass
allows to be run once before any of the test methods in the class.
@AfterClass
This will perform the method after all tests have finished. This can be used to perform clean-up some activities.
@Ignore
The @Ignore
annotation is used to ignore the test and that test will not be executed.
Practical part: Application
Right now we can start with practical task. First of all, I'll show how to create application from this example. First step is configuring project use build.gradle
file.
defaultConfig {
...
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
testCompile 'com.android.support:support-annotations:23.2.1'
compile 'com.android.support:appcompat-v7:23.2.1'
}
After it, we need to update strings.xml
file:
<resources>
<string name="app_name">SimpleCoffeeOrderTestProject</string>
<string name="coffee_price">Coffee: $%.1f</string>
<string name="total_price">Total price: $%.1f</string>
<string name="increment_label">+</string>
<string name="decrement_label">-</string>
<string name="default_coffee_count">0</string>
</resources>
Next step is updating the layout in my case it's activity_main.xml
file:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:orientation="vertical">
<TextView
android:id="@+id/coffee_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/coffee_price"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:gravity="center_vertical">
<Button
android:id="@+id/coffee_decrement"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:text="@string/decrement_label"/>
<TextView
android:id="@+id/coffee_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:textSize="32sp"
android:text="@string/default_coffee_count"/>
<Button
android:id="@+id/coffee_increment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:text="@string/increment_label"/>
</LinearLayout>
<TextView
android:id="@+id/total_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:text="@string/total_price"/>
</LinearLayout>
Next step is updating the Activity, in my case it's MainActivity
class:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private final static String COFFEE_COUNT = "coffee_count";
public final static float DEFAULT_COFFEE_PRICE = 5.0f;
private TextView mCoffeePrice;
private TextView mTotalPrice;
private TextView mCoffeeCount;
private CoffeeOrder mOrder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCoffeePrice = (TextView) findViewById(R.id.coffee_price);
mTotalPrice = (TextView) findViewById(R.id.total_price);
mCoffeeCount = (TextView) findViewById(R.id.coffee_count);
mCoffeePrice.setText(String.format(getString(R.string.coffee_price), DEFAULT_COFFEE_PRICE));
mTotalPrice.setText(String.format(getString(R.string.total_price), 0.0f));
findViewById(R.id.coffee_increment).setOnClickListener(this);
findViewById(R.id.coffee_decrement).setOnClickListener(this);
mOrder = new CoffeeOrder(DEFAULT_COFFEE_PRICE);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.coffee_increment:
mOrder.incrementCoffeeCount();
updateCoffeeCount();
updateTotalPrice();
break;
case R.id.coffee_decrement:
mOrder.decrementCoffeeCount();
updateCoffeeCount();
updateTotalPrice();
break;
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(COFFEE_COUNT, mOrder.getCoffeeCount());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null) {
mOrder.setCoffeeCount(savedInstanceState.getInt(COFFEE_COUNT));
updateCoffeeCount();
updateTotalPrice();
}
}
private void updateCoffeeCount() {
mCoffeeCount.setText(String.valueOf(mOrder.getCoffeeCount()));
}
private void updateTotalPrice() {
mTotalPrice.setText(String.format(getString(R.string.total_price), mOrder.getTotalPrice()));
}
}
The last step is creating class for managing of user order.
public class CoffeeOrder {
private float mCoffeePrice;
private int mCoffeeCount;
private float mTotalPrice;
public CoffeeOrder(float coffeePrice) {
mCoffeeCount = 0;
mTotalPrice = 0;
this.mCoffeePrice = coffeePrice;
}
public void setCoffeeCount(int count) {
if (count >= 0) {
this.mCoffeeCount = count;
}
calculateTotalPrice();
}
public int getCoffeeCount() {
return mCoffeeCount;
}
public void incrementCoffeeCount() {
mCoffeeCount++;
calculateTotalPrice();
}
public float getTotalPrice() {
return mTotalPrice;
}
public void decrementCoffeeCount() {
if (mCoffeeCount > 0) {
mCoffeeCount--;
calculateTotalPrice();
}
}
private void calculateTotalPrice() {
mTotalPrice = mCoffeePrice * mCoffeeCount;
}
}
Practical part: Testing of the application
Right now we can create some test for testing general functionality of this application:
- Coffee order object is not a null
- Set coffee count method
- Increment a value
- Decrement a value
- Calculate a total price
public class CoffeeOrderTest {
private final static float PRICE_TEST = 5.0f;
private CoffeeOrder mOrder;
@Before
public void setUp() {
mOrder = new CoffeeOrder(PRICE_TEST);
}
@Test
public void orderIsNotNull() {
assertNotNull(mOrder);
}
@Test
public void orderDecrement() {
mOrder.decrementCoffeeCount();
assertEquals(0, mOrder.getCoffeeCount());
mOrder.setCoffeeCount(25);
mOrder.decrementCoffeeCount();
assertEquals(24, mOrder.getCoffeeCount());
}
@Test
public void orderIncrement() {
mOrder.incrementCoffeeCount();
assertEquals(1, mOrder.getCoffeeCount());
mOrder.setCoffeeCount(25);
mOrder.incrementCoffeeCount();
assertEquals(26, mOrder.getCoffeeCount());
}
@Test
public void orderTotalPrice() {
assertEquals(0.0f, mOrder.getTotalPrice());
mOrder.setCoffeeCount(25);
assertEquals(PRICE_TEST * 25, mOrder.getTotalPrice());
}
@Test
public void orderSetCoffeeCount() {
mOrder.setCoffeeCount(-1);
assertEquals(0, mOrder.getCoffeeCount());
mOrder.setCoffeeCount(25);
assertEquals(25, mOrder.getCoffeeCount());
}
}
Run the tests
First way you can choose your package with test, open option and choose Run tests in {PROJECT_NAME}.
Another way is creating custom configuration, for it need to choose Run -> Edit configurations ... and create a Unit configuration.
If your project has different type of tests you must choose which test need to build in Build Variants (Android Studio 1.5). Beta version of Android Studio 2 hasn't any this option and all type of tests are building.
Source code of application (without any unit tests) you can find on GitHub.
Source code of application with unit tests you can find on GitHub.
Resources: