Using InstantTaskExecutorRule causes java.lang.RuntimeException in junit test - android

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.)

Related

Exception while computing database live data

I was wondering why I receive the error:
"java.lang.RuntimeException: exception while computing database live data."
I usually get this when running this in an androidTest:
#RunWith(AndroidJUnit4::class)
class ViewModelTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
#Test
#Throws(Exception::class)
fun tester() = runBlocking {
ActivityScenario.launch(LoginActivity::class.java)
Thread.sleep(2000)
}
}
Could someone explain why I'm getting this error? I realize that I'm not using any live data at the moment, but was wondering why I'm getting this error in the first place.
I've used livedata on unit tests and I don't seem to receive this error. Only when I try launching an activity/scenario or do UI testing.
You might want to consider upgrading your sqlite database and your sqlite libraries.

Non Instrumentation Tests for Room Database

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()
}
}

Cannot invoke observeForever on a background thread

I've been using an observeForever() method as described here to test Room and LiveData for a while, and it has worked flawlessly. But when I changed to Android Studio 3.2 (or if it was the androidx refactoring, not sure), that method suddenly stopped working, throwing a
java.lang.IllegalStateException: Cannot invoke observeForever on a background thread
How can we fix this?
I solved it by adding the rule InstantTaskExecutorRule. According to the docs it will
A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.
So one needs to add
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
to the test class for it to work. The Java equivalent would be
#Rule
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
You will also need to add
androidTestImplementation "androidx.arch.core:core-testing:2.0.0"
to your models build.gradle dependencies.
As a beginner to this approach, accepted answer was a little bit vague for me. So just trying to explain it
add this in your build.gradle
androidTestImplementation "androidx.arch.core:core-testing:2.0.0
Now we need to add rule on test function. Lets say that I have a test function writeAndReadCategory then it will look like this in kotlin
#get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
#Test
fun writeAndReadCategory() {
....
}

Testing LiveData using PowerMockRunner

My local unit tests use LiveData all the time. Normally, when you try to set a value on MutableLiveData you get
java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.
because local JVM has no access to Android framework. I fixed that using that:
#get:Rule
val rule = InstantTaskExecutorRule()
Everything was fine, until I had to use PowerMockito to mock a static method from google play library. Since I added
#RunWith(PowerMockRunner::class)
#PrepareForTest(Tasks::class)
above my test class declaration I started to get this Looper not mocked error again. I used this rule before with MockitoJUnitRunner and everything was fine.
A bit late for the answer, but just faced the same issue and solved it!
To use PowerMock and InstantTaskExecutorRule you need to add the following annotation:
#RunWith(PowerMockRunner::class)
#PowerMockRunnerDelegate(MockitoJUnitRunner::class) //this line allows you to use the powermock runner and mockito runner
#PrepareForTest(UnderTestClass::class)
class UnderTestClassTest {
#get:Rule
var instantExecutorRule = InstantTaskExecutorRule()
No need to fret as it turns out you can still use this method to test your LiveData observers!
First, add this dependency in your module’s build.gradle file:
testImplementation 'android.arch.core:core-testing:1.0.0-alpha3'
Make sure you use the same version as the rest of your android.arch.* dependencies!
Then, in the test class where you need to call setValue() and assert, add this field:
#Rule
public TestRule rule = new InstantTaskExecutorRule();
For Kotlin
#get:Rule
var rule: TestRule = InstantTaskExecutorRule()
Behind the scenes, this bypasses the main thread check, and immediately runs any tasks on your test thread, allowing for immediate and predictable calls and therefore assertions.
Already have this answer here.

Unit Testing AndroidViewModel classes

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!

Categories

Resources