A gentle introduction to unit testing in android
Unit testing aims to validate that a given part of software works exactly as designed, independently from all the other parts. This part is the smallest testable, and in procedural programming can be a function, in Object Oriented Programming, a method. Unit testing used in all the other languages, but in this article, we’re going to see how to create unit tests for an Android app. You find the full project on github here.
This app takes as input user’s height and weight, calculate their BMI (Body Mass Index) and outputs a tip accordingly (Depending on BMI, the app determines whether the user’s given weight is normal, underweight, overweight, or obese, and a tip is output as a result).
Before we start, I want you to know that there are two ways to perform unit testing in Android. The first one is unit tests that run on JVM, and are called local tests. As for the second, it is called instumented unit tests which run on Android system. In this example, We’re going to tackle only local tests, so let’s get started!
After creating the android project, we need to add a dependency that allows to perform the unit tests. In gradle dependecies, add the following line:
testImplementation 'junit:junit:4.12'
junit is a java library for unit tests and it is based on annotations.
After adding the dependency, let’s create now, in the app package, a java class “BMI”
public class BMI {
private float weight;
private float height;
public BMI(float weight, float height){
this.weight = weight;
this.height = height;
}
//This function calculates BMI using the formula kg/m²
public float calculateBMI(){
return (weight/(height*height));
}
}
The “BMI” class has a method “calculateBMI ()”, that we are going to test in the first part of this tutorial.
Now, if you have noticed, under the java directory, there are three packages: the first package contains “BMI” class and other activities, the second is written next to it, “AndroidTest”, which contains the classes that are used for instrumented unit test, and the third package, which is written next to it “Test”, should contain the test classes that are used for local tests, that run on JVM. In this package, we create a java class named “BMITest” that will perform the unit test of the “BMI” class, and then add the following code:
public class BMITest {
private BMI bmi;
//To initialize bmi before starting the test
@Before
public void init(){
bmi = new BMI( 68, 1.67f );
}
@Test
public void testCalculateBMI(){
float expectedValue = 24.38f;
assertEquals("BMI calculating is correct ", expectedValue, bmi.calculateBMI(), 0.01);
}
}
In this class, there two annotations:
- Test: To refer to the method that runs the test
- Before: to indicate that the method under this annotation, should run before starting the unit test.
So, before starting the test, we first initialize “bmi”, then we run the test. The method “testCalculateBMI()”, is calling a method “assertEquals()”, that has as first parameter, a comment, the expected value that the “BMI” method “calculateBMI()” should return, as a third arguement, we have the actual value of the method being tested (“calculateBMI()”), as for the last arguement, we used it because we’re dealing with floats, just to guarantee some precision and so that the unit test works properly. What “assertEquals()” does, is that it forces the returned value by the tested method to be equal to the expected value so that the test succeeds, or it fails if the tested method returns a value other than the expected method.
To run the test in android studio, go to the upper menu, where you usually run your app, then select the test class from the highlited arrow in the screenshot below:
Once you run your test, you should have the following result:
A green progressbar indicates that the test was successful, and it is red in case test fails for some reason.
Now we’re done with the first part, congratulations!
If you have noticed that we run a test on a method that has no dependency on any other class. However, in real cases, it is not the case, we run unit tests on methods that have many dependencies, and that’s what we’re going to see now.
Remember that the point of unit test is to validate that a given part performs the function it was designed for, independly of all the other dependecies this part has, so how can we do that? In order to do so, we’re going to use a mock framework, it is called so because it fakes the interactions of the tested part with the other parts. For instance, we want to run unit test on a method that depends on some other method, a mock framework will fake the external communication between the tested method and the method inside it, without caring if the internal methods works properly or not, it just makes sure the external communications works. We’re going to use the mockito framwork. So again, in the gradle file, add the following dependency:
testImplementation 'org.mockito:mockito-core:1.10.19'
Now, let’s create another class that depends on the “BMI” class, and it is as follow:
public class WeightTips {
private BMI bmi;
public WeightTips(BMI bmi){
this.bmi = bmi;
}
public int getTipAboutYourWeight(){
float bmiValue = bmi.calculateBMI();
if(bmiValue<18.5){
//"under weight";
return -1;
}
else if(bmiValue >= 18.5 && bmiValue <= 24.9){
//"Normal";
return 0;
}
else if(bmiValue >= 25 && bmiValue <= 29.9) {
//"Overwheight";
return 1;
}
else{
//Obese
return 2;
}
}
This class has a dependency with the “BMI” class, as it uses the method “calculateBMI()” in order to return tips according to the BMI value.
Now in the same package where we created our first unit test class, let’s create another test class for this new class in which we’ll test “getTipAboutYourWeight()”:
@RunWith(MockitoJUnitRunner.class)
public class WeightTipsTest {
@Mock
BMI bmi;
WeightTips weightTips;
@Before
public void init(){
weightTips = new WeightTips( bmi );
}
//Testing the normal state
@Test
public void getWeightTipNormalTest(){
when(bmi.calculateBMI()).thenReturn( 19.01f );
assertEquals( "normal weight",0,weightTips.getTipAboutYourWeight() );
// Read personal information from
}
//Testing the underweight state
@Test
public void getWeightTipUnderWeightTest(){
when(bmi.calculateBMI()).thenReturn( 16.1f );
assertEquals( "underweight",-1,weightTips.getTipAboutYourWeight() );
// Read personal information from
}
//Testing the overweight state
@Test
public void getWeightTipOverWeightTest(){
when(bmi.calculateBMI()).thenReturn( 26.36f );
assertEquals( "overweight",1,weightTips.getTipAboutYourWeight() );
// Read personal information from
}
//Testing the obese state
@Test
public void getWeightTipObeseTest(){
when(bmi.calculateBMI()).thenReturn( 32.36f );
assertEquals( "obese",2,weightTips.getTipAboutYourWeight() );
// Read personal information from
}
}
Now you notice that this class is proceeded by this annotation
@RunWith(MockitoJUnitRunner.class)
It allows to automatically initialize the mock objects.
In this test class, we want to make sure that “getTipAboutYourWeight()” returns the right value. The tested method can retun one of four possible values: -1 to indicate underweight, 0 to refer to a normal weight, 1 to refer to overweight, and 2 to refer to obese. We created a test method for each case. Let’s take one for example and explain it, as the same rule applies to the other cases.
//Testing the normal state
@Test
public void getWeightTipNormalTest(){
when(bmi.calculateBMI()).thenReturn( 19.01f );
assertEquals( "normal weight",0,weightTips.getTipAboutYourWeight() );
// Read personal information from
}
Here, we’re testing the case of normal weight. We used “when().thenReturn()”, in order to force “bmi.calculateBMI()” return a specified value, we’re faking ““bmi.calculateBMI()” behavior, and thus, we really don’t care if the latter works right or not as we’re focusing on testing “weightTips.getTipAboutYourWeight()” behavior. Once the “bmi.calculateBMI()” returns the value that is supposed to, now we can make sure that “weightTips.getTipAboutYourWeight()” works properly. So we call again “assertEquals” in order to make the test only succeeds if the test method returns 0 (0 indicates that the user’s weight is normal). The same thing applies to the rest cases.
Hope you enjoyed this article, and if you have any feedbacks, I would love to see them in the comment section.
Happy testing :)