Android Testing

Android testing: Unit testing (Part 1)

Alex Zhukovich 7 min read
Android testing: Unit testing (Part 1)
Table of Contents

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.

Android app for testing

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) - the assertSame() methods tests if two object references point to the same object;
  • void assertNotSame(unexpected, actual) - the assertNotSame() methods tests if two object references not point to the same object;
  • void assertArrayEquals(expectedArray, actualArray) - the assertArrayEquals() 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 &gt;= 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 &gt; 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:

  1. Coffee order object is not a null
  2. Set coffee count method
  3. Increment a value
  4. Decrement a value
  5. 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:


Mobile development with Alex

A blog about Android development & testing, Best Practices, Tips and Tricks

Share
More from Mobile development with Alex

Great! You’ve successfully signed up.

Welcome back! You've successfully signed in.

You've successfully subscribed to Mobile development with Alex.

Success! Check your email for magic link to sign-in.

Success! Your billing info has been updated.

Your billing was not updated.