Introduction
- Now that I have officially released my first app, which can be found HERE on the Google Play store. I want to add more features to my app, however, I think the next best step is to add some tests. So, this series is going to be a practical series dedicated to understanding testing in the Android framework.
Source code
- You can find my source code HERE
Video version
You can find the video version HERE
A warning!
- If you want to use the industry standard for testing and creating Asynchronous code, then use RxAndroid. However, my codebase is small enough that I believe a library is not needed. Which means we are going to be using some good old fashion CONCURRENT PROGRAMMING!!!!!!!
Threads and Processes
- Before we can go any further we need to talk about the two basic units of execution in concurrent programming:
1) Processes
2) Threads
1) Processes
- A process is a self-contained execution environment, each process has its own memory space. Depending on the environment you might have a single process or multiple processes communicating with each other
2) Threads
- Threads are sometimes called light weight processes. Both threads and processes provide an execution environment, but creating a thread requires fewer resources than creating a new process.
Threads exist within a process and every process has at least one thread.
Threads share the process's resources including memory. This makes for efficient but potentially problematic communication.
Processes and Threads in Android
- In the majority of cases, every Android application runs in its own Linux process(Android runs Linux by the way).By default all components of the same application use the same process and thread called the 'main' thread.
Main thread
When an application is launched, the system creates a thread of execution for the application called
main
. The main thread is very important because it is in charge of dispatching events to the appropriate user. It is also the thread in which the application interacts with components form the Android UI toolkit( components from the widget and view packages). Now when dealing with the main thread main thread there are two simple rules to always follow:1) do not block the main thread
2) do not access the Android UI toolkit from outside the main thread
To abide by these rules we are going to implement
worker threads
Worker threads
- To be able to keep our app responsive and not block the main thread, we make sure to do tasks that don't respond immediately in a separate thread(worker thread). Tasks such as decoding a bitmap, accessing storage, working on a machine learning model or performing a network request should be executed on a worker thread. Do not worry I will go into more detail about worker threads once we start using them.
Creating the test database
First, I have already assumed you have set up a Room database and a DAO(data access object). If you have not, you can follow the instructions HERE, comeback and continue once you have both set up.
Now make a class and call it TestDatabase:
public class TestDatabase {
// should be an instance of your real world database
private volatile CalfRoomDatabase calfDatabase;
}
- Notice the
volatile
keyword, it is used to eliminate the risk of memory consistency errors. Read more about the volatile keyword HERE. Ignore my naming convention ofCalfRoomDatabase
, replace it with whatever your Room database implementation is called. Now lets move on to creating background threads(worker threads)
Creating worker threads
public class TestDatabase {
// should be an instance of your real world database
private volatile CalfRoomDatabase calfDatabase;
private static final int NUMBER_OF_THREADS = 4;
public static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
}
- The easiest and most straight forward way to create worker threads is through a thread pool and more specifically a
fixed thread pool
. A fixed thread pool contains a specific number of threads(worker threads). We will then issue asynchronous tasks to the fixed pool, which will store the tasks via an internal queue, which will hold the task until a worker thread is available. If a worker thread is not available then the task will wait until one becomes free to do the task. We create our fixed thread pool with the statement:
public static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
- We can now have access to threads outside of the main thread to perform asynchronous tasks.
In memory database
- For testing purposes
ONLY
we will be creating an in memory database. This kind of database stores information in a memory database(on the device) that will disappear when the process is killed. This means that when the test ends so too will the database. - Now for the sake of good testing habits, we will be creating and destroying a database for each test. That way we will know the current state of each database. We will be able to achieve this with the
@After
and@Before
annotations that Junit provides for us. As you might of guessed, any method marked withBefore
will run before a test and a method marked with@After
will run after the test.
public class TestDatabase {
//... still include other methods
@Before
public void init(){
calfDatabase = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
CalfRoomDatabase.class
).build();
}
@After
public void finish(){
calfDatabase.close();
}
}
- The
@Before
method holds all the code necessary to create a database for us. This means that before every test a new database will get created. The@After
method will run after every method and destroy the database by calling.close()
The DAO
- Next we need access to the DAO (data access object). This will give us access to DAO operations which is what we need to test. If you have not created a DAO yet or are unfamiliar with its job, use the link HERE. To get access to the DAO we simply create a method:
public class TestDatabase {
//... still include other methods
public CalfDao getCalfDao(){
// getCalfDao() implemented by Room
return calfDatabase.getCalfDao();
}
}
- Again, ignore naming conventions of my code(My app is cow themed). The difference between your code and my code is that you should change out
calfDatabase
with whatever your database class is called.
THE ACTUAL TEST!!!
- Ok now that we have gotten all of the details lets create some tests. So, what we are actually testing is the DAO, because that is how we access our database. Create a new class called
DAOTest
and have it extend ourTestDatabase
:
public class DAOTest extends TestDatabase {
}
- Through class inheritance we now have access to everything inside the TestDatabase class. This way we can avoid having to deal with concrete implementations. The next step that we take is to create a test method.
public class DAOTest extends TestDatabase {
@Test
public void insertTest() throws Exception{
}
}
- The
@Test
notation is from Junit and it signifies to the system that this method is going to be used for a test. Meaning that the@Before
and@After
methods will run. Then we can create the test object that is going to be inserted and a test value that is going to be returned upon a successful insertion.
Calf calfTest1 = new Calf(1,"test-1", "TEST 1", new Date(),"Bull","test-1");
int testingReturnValue = 1;
- Now, it is time to actually get asynchronous by using a Future. A Future is an object that represents the result of an asynchronous computation. The result can only be retrieved using the method
get()
when the computation has completed, blocking until the result is achieved.
public class DAOTest extends TestDatabase {
@Test
public void insertTest() throws Exception{
Future<Integer> calf = CalfDatabaseTest
.databaseWriteExecutor.submit(new Runnable() {
@Override
public void run() {
getCalfDao().insert(calfTest1);
}
}, testingReturnValue); //RETURN A FUTURE OBJECT
int returningCalfValue = calf.get();
}
}
- We provide Future with a Integer type parameter because our
testingReturnValue
is an integer. Next we access our fixed thread pool,CalfDatabaseTest.databaseWriteExecutor
. This gives us access to the worker threads which we can assign asynchronous tasks to. We create a task through the submit method and provide it a Runnable.submit(new Runnable(),testingReturnValue)
, since the Runnable does not have a return value we provide one withtestingReturnValue
. Inside the Runnable we override therun
method and call the task that we want done on the worker thread. Since this code is asynchronous, we need to block and wait for the value. We do that with theget()
method. Theget()
method will block until it gets a return value.
Assert
- Lastly we can make the assertion that the
returningCalfValue
is the same as thetestingReturnValue
thus indicating a successful insertion.
Assert.assertEquals(testingReturnValue,returningCalfValue);
- With this we can hit the little green arrow next to the method to run the test and see that it has passed.
Conclusion
- Thank you for taking the time out of your day to read this blog post of mine. If you have any questions or concerns please comment below or reach out to me on Twitter.
Top comments (0)