I'm writing tests (not instrumentation tests) for the service layer. I'd like to use the actual DAO layer instead of mocks as this makes service layer tests more functional (IMHO). I know how to create an in-memory room db for an instrumentation test:
val context = ApplicationProvider.getApplicationContext<Context>()
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java).build()
However, this won't work in the other tests as the call to getApplicationContext makes a call to the InstrumentationRegistry which won't work in non-instrumentation tests. I realize that the room tests should be instrumentation tests; they are. All DAO tests are instrumentation tests which aim to test out the queries which have been written. I also realize that these aren't technically unit tests; I'm okay with that. In my experience, service layer tests which do not mock the repository layer are less brittle than those which do. Anyway, my question is - how can I accomplish this goal? Is there a way to retrieve the application context without instrumentation? Is there a room db stand-in that doesn't require the application context? Or do I need to implement another version of the DAO classes for the tests?
robolectric will allow you to run this kind of test.
#RunWith(RobolectricTestRunner::class)
#Config(sdk = [27])
class Db {
private lateinit var db: AppDatabase
private lateinit var myDao: myDaoType
#Before
fun createDB() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java)
.fallbackToDestructiveMigration().allowMainThreadQueries().build()
myDao = db.myDao()
}
#After
fun closeDb() {
db.close()
}
}
Related
How to write JUnit test cases for FirebaseRemoteConfig in android using Mockito.
I have tried this so far:
class MyUtilClassTest {
#Test
fun testKeyValue() {
val context = mock(Context::class.java)
FirebaseApp.initializeApp(Context)
val keyValue = MyUtilClass.getKeyValue("keyName")
assertTrue(keyValue)
}
}
object MyUtilClass {
fun getKeyValue(key: String) {
FirebaseRemoteConfig.getInstance().getString(documentName)
...
}
}
But getting this exception:
java.lang.NullPointerException
at com.google.android.gms.common.internal.StringResourceValueReader.<init>(com.google.android.gms:play-services-basement##17.3.0:5)
at com.google.firebase.FirebaseOptions.fromResource(FirebaseOptions.java:156)
at com.google.firebase.FirebaseApp.initializeApp(FirebaseApp.java:242)
I would strongly recommend using Mockito to mock the MyUtilClass methods that rely on Firebase Remote Config rather than using it directly in your unit tests.
For example, something like this could work (apologies if the Mockito usage is
not 100% accurate, it's been a while):
#Test
public void myTest {
MyUtilClass mockUtilClass = Mockito.mock(MyUtilClass.class);
Mockito.when(mockUtilClass.getKeyValue(eq("keyName")).thenReturn("expectedValue"));
// Run test with mocked util class.
testCodePath()
}
Note that while it makes sense to test Remote Config in a QA environment when deployed to tester devices, it's almost never a good idea to test Remote Config itself in a unit test. Rather than doing that, you can hardcode or mock the values you expect to be returned from Remote Config, and test your function or component based on those value sets.
I am unit-testing a library that uses a RoomDatabase. Unit-testing the RoomDatabase itself was done using InstantTaskExecutorRule so that LiveData updates can occur instantaneously:
#Rule public TestRule rule = new InstantTaskExecutorRule();
And that works fine. However, when unit-testing the library which calls the database, using the same rule causes the test to throw the exception:
java.lang.RuntimeException: Exception while computing database live data.
...
Caused by: java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
Is there any reason why InstantTaskExecutorRule works on the underlying database DAOs but not a library calling them?
I am also unit testing a RoomDatabase with Robolectric and the same error occurs while using
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
I simply removed it and I'm using runBlocking{ ... } in my tests like this example :
#Test
#Throws(Exception::class)
fun `test insert and retrieve text`() {
runBlocking {
dao.insertText("some text")
assertEquals("some text",dao.getText())
}
}
The only reason I can think of causing this error is a missing ".allowMainThreadQueries()" when building your database:
database = Room.inMemoryDatabaseBuilder(
ApplicationProvider.getApplicationContext(),
myDatabase::class.java
)
.allowMainThreadQueries()
.build()
(Also, testing with databases is recommended to be done as instrumented tests as local tests will use a default SQLite version instead of the version your app is actually using.)
I have a helper class to save user object to shared preferences. I have used a serialize(): String function and a create(serializedString: String) function in my User data model. They use GSon serializer and are working good as suggested by the unit tests on them.
Now my helper class is called SharedPreferenceUserStore.kt which takes a Context object. The code is:
class SharedPreferenceUserStore(context: Context) {
companion object {
val TAG = SharedPreferenceUserStore::class.java.simpleName
}
var userLocalSharedPref: SharedPreferences =
context.getSharedPreferences(USER_LOCAL_STORE_SHARED_PREF_NAME, Context.MODE_PRIVATE)
/*
Store the required data to shared preference
*/
#SuppressLint("ApplySharedPref")
fun storeUserData(user: User) {
val userLocalDatabaseEditor = userLocalSharedPref.edit()
val serializedData = user.serialize()
userLocalDatabaseEditor.putString(
USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY,
serializedData
)
if (userLocalDatabaseEditor.commit()) {
Log.d(TAG, " Store Commit return true")
}
}
/*
Clear all the locally stored data from the shared pref
*/
#SuppressLint("ApplySharedPref")
fun clearUserData() {
val userLocalDatabaseEditor = userLocalSharedPref.edit()
userLocalDatabaseEditor.clear()
userLocalDatabaseEditor.commit()
}
fun getLoggedInUser(): User? {
val stringUser = userLocalSharedPref.getString(
USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")
return if (stringUser==null || stringUser == ""){
null
} else{
User.create(stringUser)
}
}
And I have written some unit tests for this helper class as follows:
#RunWith(JUnit4::class)
class SharedPreferenceUserStoreTest {
lateinit var sharedPreferenceUserStore: SharedPreferenceUserStore
lateinit var user: User
//to be mocked
lateinit var sharedPreferences: SharedPreferences
lateinit var sharedPreferencesEditor: SharedPreferences.Editor
lateinit var context: Context
#Before
fun setUp() {
//mocking Context and SharedPreferences class
context = mock(Context::class.java)
sharedPreferences = mock(SharedPreferences::class.java)
sharedPreferencesEditor = mock(SharedPreferences.Editor::class.java)
//specifying that the context.getSharedPreferences() method call should return the mocked sharedpref
`when`<SharedPreferences>(context.getSharedPreferences(anyString(), anyInt()))
.thenReturn(sharedPreferences)
//specifying that the sharedPreferences.edit() method call should return the mocked sharedpref editor
`when`(sharedPreferences.edit()).thenReturn(sharedPreferencesEditor)
//specifying that the sharedPreferencesEditor.putString() method call should return the mocked sharedpref Editor
`when`(sharedPreferencesEditor.putString(anyString(), anyString())).thenReturn(
sharedPreferencesEditor
)
`when`(sharedPreferences.getString(anyString(), anyString())).thenReturn("")
//instantiating SharedPreferenceUserStore from the mocked context
sharedPreferenceUserStore = SharedPreferenceUserStore(context)
user = User(
35,
"Prashanna Bhandary",
"prashanna.bhandary#gmail.com",
"dd58a617ea618010c2052cb54079ad67.jpeg",
"98********",
"test address 01",
1,
"yes",
"2019-08-30 04:56:43",
"2019-08-30 05:14:47",
0
)
}
#After
fun tearDown() {
}
#Test
fun passUser_storeUserData() {
sharedPreferenceUserStore.storeUserData(user)
verify(sharedPreferencesEditor).putString(
Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY,
user.serialize()
)
verify(sharedPreferencesEditor).commit()
}
#Test
fun testClearUserData() {
sharedPreferenceUserStore.clearUserData()
verify(sharedPreferencesEditor).clear()
}
#Test
fun testGetLoggedInUser_storeNotCalled() {
//calling getLoggedInUser() without calling storeUserData() should give null
assertEquals(null, sharedPreferenceUserStore.getLoggedInUser())
//verify that getString() was called on the shared preferences
verify(sharedPreferences).getString(Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")
}
#Test
fun testGetLoggedInUser_storeCalled(){
//call getLoggedInUser(), we are expecting null
assertNull(sharedPreferenceUserStore.getLoggedInUser())
//verify that getString() was called on the shared preferences
verify(sharedPreferences).getString(Constants.USER_LOCAL_STORE_SHARED_PREF_SERIALIZED_DATA_KEY, "")
}
}
As I am really new to Unit Testing and Mocking libraries like Mockito. Now my question is are my tests any good? and I wanted to test if the getLoggedInUser() funciton of my helper class is doing what it is supposed to do (ie. get logged in user if shared pref has it), how do I do that?
In addition do suggest me any improvements I can make to my test or the helper class itself. Thank you.
Judging your test for what it is - A unit test running on a host machine with Android dependencies mocked with Mockito - it looks fine and like what you would expect.
The benefit-to-effort ratio of such tests are debatable, though. Personally I think it would be more valuable to run such a test against the real SharedPreferences implementation on a device, and assert on actual side effects instead of verifying on mocks. This has a couple of benefits over mocked tests:
You don't have to re-implement SharedPreferences with mocking
You know that SharedPreferenceUserStore will work with the real SharedPreferences implementation
But, such tests also have a debatable benefit-to-effort ratio. For a solo developer project, think about what kind of testing that is most important. Your time is limited so you will only have time to spend on writing the most important kind of tests.
The most important kinds of tests are the ones that test your app in the same way your users will use it. In other words, write high-level UI Automator tests. You can write how many mocked or on-device unit tests as you want. If you don't test that your entire app as a whole works, you will not know that it works. And if you don't know that your app as a whole works, you can't ship it. So in some way you have to test your app in its entirety. Doing it manually quickly becomes very labour intensive as you add more and more functionality. The only way to continually test your app is to automate the high-level UI testing of your app. That way you will also get code coverage that matters.
One big benefit of high-level UI testing that is worth pointing out is that you don't have to change them whenever you change some implementation detail in your app. If you have lots of mocked unit tests, you will have to spend a lot of time to refactor your unit tests as you refactor the real app code, which can be very time consuming, and thus a bad idea if you are a solo developer. Your UI Automator tests do not depend on low-level implementation details and will thus remain the same even if you change implementation details.
For example, maybe in the future you want to use Room from Android Jetpack to store your user data instead of SharedPreference. You will be able to do that without changing your high level UI tests at all. And they will be a great way to regression test such a change. If all you have are mocked unit tests, it will be a lot of work to rewrite all relevant unit tests to work with Room instead.
I agree with what #Enselic say about favoring Integration Test over Unit Tests.
However I disagree with his statement that this mockito test looks fine.
The reason for that is that (almost) every line in your code under test involves a mock operation. Basically mocking the complete method would have the same result.
What you are doing in your test is testing that mockito works as expected, which is something you should not need to test.
On the other hand your test is a complete mirror of the implementation itself. Which means everytime you refactor something, you have to touch the test. Preferably would be a black box test.
If you use Mockito you should try to restrict its use to methods that actually do something (that is not mocked).
Classes that generally should be mocked for testing purposes are dependencies that interact with external components (like a database or a webservice), however in these cases you are normally required to have Integration Tests as well.
And if your Integration-Tests already cover most part of the code, you can check whether you want to add a test using a mock for those parts that are not covered.
I have no official source for what I am trying to express, its just based on my experience (and therefore my own opinion). Treat it as such.
There is not much that can be said regarding the tests that guys before me haven't said.
However, one thing that you might want to consider is refactoring your SharedPreferenceUserStore to accept not Context(which is quite a huge thing, and if not handled properly could lead to unforeseen issues and/or memory leaks), but rather SharedPreferences themselves. This way, your class, that deals only with updating the prefs doesn't have access to more than it should.
I'm trying to make some Unit tests for my business logic.
Data is read and written to Room database, so the logic depends on what's inside my database.
I can easily buildInMemoryDatabase and test all the logic, but using Instrumental tests which are slow and require a device to be connected.
I want to run Unit tests only where I replace my RoomRepository with some other implementation of Repository interface
class RoomRepository(
private val database: RoomDatabase //actual room database
): Repository {
override fun getFooByType(type: Int): Maybe<List<Item>> {
return database.fooDao()
.getFooByType(type)
.map { names ->
names.map { name -> Item(name) }
}
.subscribeOn(Schedulers.io())
}
}
Maybe there is a way to run Room sqlite on host machine?
Maybe there is another solution?
Create a wrapper class named "Repository" around the RoomDatabase and have methods to expose the Dao objects. By that way, we can easily mock the repository class like below
Open class Repository(private val roomDatabase:RoomDatabase){
open fun productsDao():ProductsDao = roomDatabase.productDao()
open fun clientsDao():ClientsDao = roomDatabase.clientsDao()
//additional repository logic here if you want
}
Now, in tests, this class can be easily mocked like
val repositoryMock = mock(Repository::class.java)
val productsDaoMock = mock(ProductsDao::class.java)
when(repositoryMock.productsDao()).thenReturn(productsDaoMock)
when(productsDaoMock.getProducts()).thenReturn(listof("ball","pen")
So, inject and use the repository class instead of RoomDatabase class in all the places of your project so that the repository and all the Dao can be easily mocked
You usually access the database through the #Dao interfaces. These can be mocked easily.
The daos are returned from abstract methods of your actual RoomDatabase, so this could be mocked easily as well.
Just instantiate your RoomRepository with the mocks and setup these properly.
Please refer to this article.
https://developer.android.com/training/data-storage/room/testing-db
It's Unit test and not slowly like UI Test as you think.
The recommended approach for testing your database implementation is writing a JUnit test that runs on an Android device. Because these tests don't require creating an activity, they should be faster to execute than your UI tests.
Im writting Unit Tests for my app and I've found a "speed bump" while writting them. While testing subclasses of AndroidViewModel im missing the Application parameter for its initialization. I've already read this question that uses Robolectric.
This is what I already tried so far:
Using Robolectric as the question describes. As far as I understand, Robolectric can use your custom Application class for testing, the thing is im not using a custom application class as I dont need it. (the app is not that complex).
Using mockito. Mockito throws an exception saying that the Context class cannot be mocked.
Using the InstrumentationRegistry. I moved the test classes from the test folder to the androidTest folder, giving me access to the androidTestImplementation dependencies, I tried using the InstrumentationRegistry.getContext() and parse it to Application, of course this didn't worked, throwing a cast exception. I felt so dumb trying this but again, it was worth the shot.
I just want to instanciate my AndroidViewModel classes so I can call their public methods, but the Application parameter is needed. What can I do for this?
fun someTest() {
val testViewModel = MyViewModelThatExtendsFromAndroidViewModel(**missing application parameter**)
testViewModel.foo() // The code never reaches here as the testViewModel cant be initializated
}
I had the same issue, and found two solutions.
You can use Robolectric in a Unit Test, inside test directory, and choose the platform Application class.
#RunWith(RobolectricTestRunner::class)
#Config(application = Application::class)
class ViewModelTest {
#Test
#Throws(Exception::class)
fun someTest() {
val application = RuntimeEnvironment.application
val testViewModel = MyViewModelThatExtendsFromAndroidViewModel(application)
testViewModel.foo()
}
}
Or you can use an InstrumentationTest inside androidTest directory, and cast the InstrumentationRegistry.getTargetContext().applicationContext to Application:
#RunWith(AndroidJUnit4::class)
class ViewModelTest {
#Test
#Throws(Exception::class)
fun someTest() {
val application = ApplicationProvider.getApplicationContext() as Application
val testViewModel = MyViewModelThatExtendsFromAndroidViewModel(application)
testViewModel.foo()
}
}
Hope it helped!