Issue#13 Finish Robolectric SharedPreferences test description
This commit is contained in:
parent
d29207be12
commit
c38e608c8c
6 changed files with 281 additions and 52 deletions
|
|
@ -0,0 +1,28 @@
|
|||
package org.fnives.test.showcase.storage
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Disabled
|
||||
|
||||
@Disabled("CodeKata")
|
||||
class CodeKataUserDataLocalStorageTest {
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
}
|
||||
|
||||
/** GIVEN session value WHEN accessed THEN it's returned **/
|
||||
@Test
|
||||
fun sessionSetWillStayBeKept() {
|
||||
}
|
||||
|
||||
/** GIVEN null value WHEN accessed THEN it's null **/
|
||||
@Test
|
||||
fun sessionSetToNullWillStayNull() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package org.fnives.test.showcase.storage
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import org.fnives.test.showcase.core.integration.fake.FakeUserDataLocalStorage
|
||||
import org.fnives.test.showcase.core.storage.UserDataLocalStorage
|
||||
import org.fnives.test.showcase.model.session.Session
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.koin.core.context.GlobalContext.stopKoin
|
||||
import org.koin.test.KoinTest
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
class UserDataLocalStorageTest(
|
||||
private val userDataLocalStorageFactory: () -> UserDataLocalStorage
|
||||
) : KoinTest {
|
||||
|
||||
private lateinit var userDataLocalStorage: UserDataLocalStorage
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
userDataLocalStorage = userDataLocalStorageFactory.invoke()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
|
||||
/** GIVEN session value WHEN accessed THEN it's returned **/
|
||||
@Test
|
||||
fun sessionSetWillStayBeKept() {
|
||||
val session = Session(accessToken = "a", refreshToken = "b")
|
||||
userDataLocalStorage.session = session
|
||||
|
||||
val actual = userDataLocalStorage.session
|
||||
|
||||
Assert.assertEquals(session, actual)
|
||||
}
|
||||
|
||||
/** GIVEN null value WHEN accessed THEN it's null **/
|
||||
@Test
|
||||
fun sessionSetToNullWillStayNull() {
|
||||
userDataLocalStorage.session = Session(accessToken = "a", refreshToken = "b")
|
||||
|
||||
userDataLocalStorage.session = null
|
||||
val actual = userDataLocalStorage.session
|
||||
|
||||
Assert.assertEquals(null, actual)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun createFake(): UserDataLocalStorage = FakeUserDataLocalStorage()
|
||||
|
||||
private fun createReal(): UserDataLocalStorage {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
return SharedPreferencesManagerImpl.create(context)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@ParameterizedRobolectricTestRunner.Parameters
|
||||
fun userDataLocalStorageFactories(): List<() -> UserDataLocalStorage> = listOf(
|
||||
::createFake,
|
||||
::createReal
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package org.fnives.test.showcase.storage.favourite
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -11,6 +10,7 @@ import kotlinx.coroutines.test.TestCoroutineScheduler
|
|||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.advanceUntilIdle
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.fnives.test.showcase.core.integration.fake.FakeFavouriteContentLocalStorage
|
||||
import org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage
|
||||
import org.fnives.test.showcase.model.content.ContentId
|
||||
import org.fnives.test.showcase.storage.database.DatabaseInitialization
|
||||
|
|
@ -21,20 +21,24 @@ import org.junit.Test
|
|||
import org.junit.runner.RunWith
|
||||
import org.koin.core.context.stopKoin
|
||||
import org.koin.test.KoinTest
|
||||
import org.koin.test.inject
|
||||
import org.koin.test.get
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
|
||||
@Suppress("TestFunctionName")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
internal class FavouriteContentLocalStorageImplInstrumentedTest : KoinTest {
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
internal class FavouriteContentLocalStorageImplInstrumentedTest(
|
||||
private val favouriteContentLocalStorageFactory: KoinTest.() -> FavouriteContentLocalStorage
|
||||
) : KoinTest {
|
||||
|
||||
private val sut by inject<FavouriteContentLocalStorage>()
|
||||
private lateinit var sut: FavouriteContentLocalStorage
|
||||
private lateinit var testDispatcher: TestDispatcher
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
testDispatcher = StandardTestDispatcher(TestCoroutineScheduler())
|
||||
DatabaseInitialization.dispatcher = testDispatcher
|
||||
sut = favouriteContentLocalStorageFactory()
|
||||
}
|
||||
|
||||
@After
|
||||
|
|
@ -117,4 +121,18 @@ internal class FavouriteContentLocalStorageImplInstrumentedTest : KoinTest {
|
|||
Assert.assertFalse(actual.isCompleted)
|
||||
actual.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private fun createFake(): FavouriteContentLocalStorage = FakeFavouriteContentLocalStorage()
|
||||
|
||||
private fun KoinTest.createReal(): FavouriteContentLocalStorage = get()
|
||||
|
||||
@JvmStatic
|
||||
@ParameterizedRobolectricTestRunner.Parameters
|
||||
fun favouriteContentLocalStorageFactories(): List<KoinTest.() -> FavouriteContentLocalStorage> = listOf(
|
||||
{ createFake() },
|
||||
{ createReal() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# 5. Starting of Robolectric testing
|
||||
|
||||
So we are finally here, so far we didn't had to touch any kind of context or resources, activities, fragments or anything android. This is where we have to get back to reality and actually deal with Android.
|
||||
So we are finally here, so far we didn't had to touch any kind of context or resources, activities, fragments or anything Android. This is where we have to get back to reality and actually deal with Android.
|
||||
|
||||
In this testing instruction set you will learn how to write simple tests using Robolectric.
|
||||
|
||||
|
|
@ -10,6 +10,146 @@ In this testing instruction set you will learn how to write simple tests using R
|
|||
- Learn what a Robolectric Shadow is
|
||||
- And Learn how to write basic UI tests
|
||||
|
||||
## `CodeKataUserDataLocalStorageTest`
|
||||
|
||||
Let's start with something easy:
|
||||
Our System Under Test will be `org.fnives.test.showcase.storage.SharedPreferencesManagerImpl`
|
||||
But we only test their interface functions.
|
||||
|
||||
We don't add anything Robolectric just yet, let's try to do this without it first.
|
||||
|
||||
Let's setup or System Under Test as usual:
|
||||
```kotlin
|
||||
private lateinit var sut: UserDataLocalStorage
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
sut = SharedPreferencesManagerImpl.create(mock())
|
||||
}
|
||||
```
|
||||
|
||||
And if we run our test class we already get an exception:
|
||||
|
||||
> sharedPreferences must not be null
|
||||
> java.lang.NullPointerException: sharedPreferences must not be null
|
||||
at org.fnives.test.showcase.storage.SharedPreferencesManagerImpl$Companion.create(SharedPreferencesManagerImpl.kt:65)
|
||||
|
||||
So we need to mock the creation of `SharedPreferences`, then the `SharedPreferences` as well.
|
||||
Since our classes main purpose is to handle `SharedPreferences`, that doesn't really make sense.
|
||||
|
||||
Well, I would rather not do that. So then we need to test on a Real Device or Emulator. Well we could, but then we need to integrate a Testing Farm with our CI. It would be good to do that, but sometimes that's just not possible, here is where [Robolectric](http://robolectric.org/) comes in.
|
||||
|
||||
>Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators.
|
||||
|
||||
### Setup with Robolectric
|
||||
|
||||
We already have the dependencies in the project.
|
||||
We need to annotate our class with `@RunWith(AndroidJUnit4::class)`
|
||||
With this Robolectric actually starts our `TestShowcaseApplication` so we need to stop Koin after our tests:
|
||||
```kotlin
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class CodeKataUserDataLocalStorageTest: KoinTest {
|
||||
|
||||
//...
|
||||
@After
|
||||
fun tearDown() {
|
||||
stopKoin()
|
||||
}
|
||||
```
|
||||
|
||||
Okay, now we just need to get a context. With Robolectric we can get our application class the following way:
|
||||
|
||||
```kotlin
|
||||
val application = ApplicationProvider.getApplicationContext<Application>()
|
||||
sut = SharedPreferencesManagerImpl.create(application)
|
||||
```
|
||||
|
||||
With that, we can start testing:
|
||||
|
||||
### 1. `sessionSetWillStayBeKept`
|
||||
|
||||
Well, our tests will be pretty simple since the interface itsell will be pretty simple.
|
||||
We set a value and we just verify its kept:
|
||||
|
||||
```kotlin
|
||||
val session = Session(accessToken = "a", refreshToken = "b")
|
||||
sut.session = session
|
||||
|
||||
val actual = sut.session
|
||||
|
||||
Assert.assertEquals(session, actual)
|
||||
```
|
||||
|
||||
With that our first test is already done,
|
||||
|
||||
### 2. `sessionSetToNullWillStayNull`
|
||||
|
||||
Here we almost have the same test, we just use null. Personally I also set the value beforehand.
|
||||
But you should be able to do this easily on your own. For completeness sake:
|
||||
```kotlin
|
||||
sut.session = Session(accessToken = "a", refreshToken = "b")
|
||||
|
||||
sut.session = null
|
||||
val actual = sut.session
|
||||
|
||||
Assert.assertEquals(null, actual)
|
||||
```
|
||||
|
||||
### 3. Fake
|
||||
|
||||
So if you are doing these instructions in order, you may remember that in our core integration tests, namely `org.fnives.test.showcase.core.integration.CodeKataAuthIntegrationTest` we actually had Fake implementation of this class.
|
||||
But we never verified that the Fake behaves exactly as will the real thing, so let's do that.
|
||||
Sadly we can't depend on the `org.fnives.test.showcase.core.integration.fake.CodeKataUserDataLocalStorage` since it's in a test module.
|
||||
However with usage of testFixtures we are able to share test classes as we had previously shared an Extension.
|
||||
Take a look `at code/src/testFixtures/java`, in package `org.fnives.test.showcase.core.integration.fake` We have a `FakeUserDataLocalStorage`. We can use that since it's in the testFixture.
|
||||
|
||||
> Reminder: Test fixture plugin creates a new testFixture sourceset where main <- testFixture <- test dependency is created.
|
||||
> Also one can depend on another modules testFixtures via testImplementation testFixtures(project('<moduleName>'))
|
||||
|
||||
So what's better way is there to verify the `Fake` than testing it with the `Real` implementation's test case?
|
||||
|
||||
To do that we will parametrize our test. Note, it will be different than previous, since it's junit4 and Robolectric.
|
||||
|
||||
Let's modify our annotation and Test Class constructor:
|
||||
```kotlin
|
||||
@RunWith(ParameterizedRobolectricTestRunner::class)
|
||||
class CodeKataUserDataLocalStorageTest(val userDataLocalStorageFactory: () -> UserDataLocalStorage) : TestKoin {
|
||||
//...
|
||||
}
|
||||
```
|
||||
|
||||
Then we create our parameters:
|
||||
```kotlin
|
||||
companion object {
|
||||
|
||||
private fun createFake(): UserDataLocalStorage = FakeUserDataLocalStorage()
|
||||
|
||||
private fun createReal(): UserDataLocalStorage {
|
||||
val context = ApplicationProvider.getApplicationContext<Context>()
|
||||
|
||||
return SharedPreferencesManagerImpl.create(context)
|
||||
}
|
||||
|
||||
@JvmStatic // notice it needs to be static
|
||||
@ParameterizedRobolectricTestRunner.Parameters // notice the annotation
|
||||
// notice the return List's type parameter matches the constructor of CodeKataUserDataLocalStorageTest
|
||||
fun userDataLocalStorageFactories(): List<() -> UserDataLocalStorage> = listOf(
|
||||
::createFake,
|
||||
::createReal
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Now we just change how we create our SUT:
|
||||
```kotlin
|
||||
@Before
|
||||
fun setup() {
|
||||
sut = userDataLocalStorageFactory.invoke()
|
||||
}
|
||||
```
|
||||
|
||||
Now we validated our fake implementation as well. With this we can be sure our previous integration tests were indeed correct.
|
||||
|
||||
## FavouriteContentLocalStorage test
|
||||
|
||||
Our System Under Test will be `org.fnives.test.showcase.core.storage.content.FavouriteContentLocalStorage` or more precisely it's implementation: `org.fnives.test.showcase.storage.favourite.FavouriteContentLocalStorageImpl`
|
||||
|
|
@ -25,47 +165,12 @@ So let's start with the setup.
|
|||
|
||||
Our test class is `org.fnives.test.showcase.storage.favourite.CodeKataFavouriteContentLocalStorageInstrumentedTest`
|
||||
|
||||
Question: Why don't we test the DAO and Storage separately using mocking?
|
||||
Answer: The same logic applies how we didn't test the RetrofitServices just the RemoteSources. The Service just like the DAO is an implementation detail, our code only accesses them through the RemoteSource / LocalStorage abstraction. With this in mind now we only want to test that we interact with the database properly, we don't really care how many DAOs are used.
|
||||
> Question: Why don't we test the DAO and Storage separately using mocking?
|
||||
|
||||
We don't add anything Robolectric just yet, let's try to do this without it first.
|
||||
>Answer: The same logic applies how we didn't test the RetrofitServices just the RemoteSources. The Service just like the DAO is an implementation detail, our code only accesses them through the RemoteSource / LocalStorage abstraction. With this in mind now we only want to test that we interact with the database properly, we don't really care how many DAOs are used.
|
||||
|
||||
Let's setup or System Under Test as usual:
|
||||
We again need Robolectric to create a Room Database.
|
||||
|
||||
```kotlin
|
||||
private lateinit var sut: FavouriteContentLocalStorage // notice we only care about the interface
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
val room = Room.inMemoryDatabaseBuilder(mock(), LocalDatabase::class.java) // we are using inmemory, cause we don't really want to create files.
|
||||
.allowMainThreadQueries() // we don't really care about threading for now
|
||||
.build()
|
||||
|
||||
sut = FavouriteContentLocalStorageImpl(room.favouriteDao)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun atTheStartOurDatabaseIsEmpty() = runBlocking<Unit> {
|
||||
// we just verify our setup is correct
|
||||
sut.observeFavourites().first()
|
||||
}
|
||||
```
|
||||
|
||||
Let's run our test and see:
|
||||
|
||||
> Method getWritableDatabase in android.database.sqlite.SQLiteOpenHelper not mocked. See http://g.co/androidstudio/not-mocked for details.
|
||||
> java.lang.RuntimeException: Method getWritableDatabase in android.database.sqlite.SQLiteOpenHelper not mocked. See http://g.co/androidstudio/not-mocked for details.
|
||||
> at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java)
|
||||
|
||||
|
||||
So we need to mock something inside the `SQLiteOpenHelper` which is used inside the Dao and Room in order to test the Database.
|
||||
Well, I would rather not do that. So then we need to test on a Real Device or Emulator. Well we could, but then we need to integrate a Testing Farm with our CI. It would be good to do that, but sometimes that's just not possible, here is where [Robolectric](http://robolectric.org/) comes in.
|
||||
|
||||
>Robolectric is the industry-standard unit testing framework for Android. With Robolectric, your tests run in a simulated Android environment inside a JVM, without the overhead and flakiness of an emulator. Robolectric tests routinely run 10x faster than those on cold-started emulators.
|
||||
|
||||
### Setup with Robolectric
|
||||
|
||||
We already have the dependencies in the project.
|
||||
We need to annotate our class with `@RunWith(AndroidJUnit4::class)`
|
||||
With this Robolectric actually starts our `TestShowcaseApplication` so instead of creating our SUT, we just inject it. However to easily inject with Koin, we extend `KoinTest`:
|
||||
```kotlin
|
||||
|
|
@ -73,14 +178,12 @@ With this Robolectric actually starts our `TestShowcaseApplication` so instead o
|
|||
class CodeKataFavouriteContentLocalStorage: KoinTest
|
||||
```
|
||||
|
||||
So additional changes will be:
|
||||
- remove our previous mocking attempt
|
||||
- we inject our SUT
|
||||
- we stop koin in tearDown
|
||||
- we add a testDispatcher to Room
|
||||
- we switch to runTest(testDispatcher)
|
||||
|
||||
Since Room has their own exercutors, that could make our tests flaky, since we might get out of sync. Luckily we can switch out these executors, so we do that to make sure our tests run just as we would like them to.
|
||||
Since Room has their own exercutors, that could make our tests flaky, since it might get out of sync. Luckily we can switch out these executors, so we do that to make sure our tests run just as we would like them to.
|
||||
|
||||
```
|
||||
private val sut by inject<FavouriteContentLocalStorage>()
|
||||
|
|
@ -103,13 +206,12 @@ fun atTheStartOurDatabaseIsEmpty()= runTest(testDispatcher) {
|
|||
}
|
||||
```
|
||||
|
||||
The line `DatabaseInitialization.dispatcher = testDispatcher` may look a bit mysterious, but all we do her is overwrite our iriginal DatabaseInitialization in tests, and use the given Dispatcher as an executor for Room setup.
|
||||
|
||||
Now if we run our test we see we can indeed access the database. We can get down to actual testing.
|
||||
The line `DatabaseInitialization.dispatcher = testDispatcher` may look a bit mysterious, but all we do her is overwrite our original DatabaseInitialization in tests, and use the given Dispatcher as an executor for Room setup.
|
||||
|
||||
### 1. `atTheStartOurDatabaseIsEmpty`
|
||||
|
||||
Since we used this test for our setup, we just need to finish it. We just verify the returned list is empty, so:
|
||||
Our test is as simple as it gets. We get the observable and it's first element. Then we assert that it is an empty list.
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun atTheStartOurDatabaseIsEmpty() = runTest(testDispatcher) {
|
||||
|
|
@ -187,7 +289,7 @@ Assert.assertEquals(expected, actual.getCompleted())
|
|||
|
||||
##### Note: we can use turbine as well to verify our flows, just like we did previously
|
||||
|
||||
### 5. `removingFavouriteUpdatesExistingObservers`
|
||||
### 5. `removingFavouriteUpdatesExistingObservers`
|
||||
|
||||
Okay, this should be really similar to `addingFavouriteUpdatesExistingObservers` just with a hint of `contentIdAddedThenRemovedCanNoLongerBeReadOut` so try to write it on your own.
|
||||
|
||||
|
|
@ -233,5 +335,13 @@ actual.cancel()
|
|||
|
||||
With that we know how to verify our Database running on the JVM, without needing an emulator or device.
|
||||
|
||||
### Fake
|
||||
|
||||
We also have created a `FakeFavouriteContentLocalStorage` previously. We can verify that also using the same parameterization.
|
||||
However this is an optional exercise.
|
||||
> Hint: we can use KoinTest.() -> T lambdas as well. And KoinTest.get() function.
|
||||
|
||||
If you want to check it out, `FavouriteContentLocalStorageImplInstrumentedTest` does exactly that.
|
||||
|
||||
## Conclusion
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue