Sign in Go Pro

Android Clean Architecture

Testing the projects cache implementation

This lesson is for PRO members.

Upgrade today to get access to all the PRO lessons.

Unlock this lesson
Autoplay

Up next

Previous

About

The projects cache implementation class is used to orchestrate the flow of data from our Room Database to our data layer. In this lesson, we're going to be writing unit tests for this class to ensure that it remains functioning as intended.

Instructor

Links

Comments

I cannot get the getProjectsReturnsData unit test to pass. I get an error that reads:

java.lang.AssertionError: Expected: com.loc8r.data.models.ProjectEntity@141d3d43, Actual: com.loc8r.data.models.ProjectEntity@6ca372ef (latch = 1, values = 1, errors = 0, completions = 0)

I tried converting the code to a mutableListOf, and I still got an error

Wow found the error. In your code you made the ProjectEntity class a "data class", and I had it as just a "class". Making the change fixed my problem. Though in my testing I downloaded your code and found that 2 of your unit tests (that I have not yet built) are failing:

  1. isProjectsCacheExpiredReturnsNotExpired
  2. getBookmarkedProjectsReturnsData

I'll guess I'll troubleshoot it in a hour when I get to it.

Well the error in your test is this:

@Test
fun getBookmarkedProjectsReturnsData() {
val bookmarkedProject = ProjectDataFactory.makeBookmarkedProjectEntity()
val projects = listOf(ProjectDataFactory.makeProjectEntity(),
bookmarkedProject)
cache.saveProjects(projects).test()

val testObserver = cache.getBookmarkedProjects().test()
testObserver.assertValue(listOf(bookmarkedProject))

ProjectDataFactory.makeProjectEntity() randomly assigns the isBookmarked field. So 50% of the time it IS bookmarked, in which case your unit test will fail. I just created a new DataFactory function makeNonBookmarkedProjectEntity() to fix the problem.

Ah yes, it must have just been luck whenever mine were passing. In the factory we should have a function for ProjectDataFactory.makeProjectEntity() and ProjectDataFactory.makeBookmarkedProjectEntity() to create the kind of project that we want with the correct bookmark status

And the problem with isProjectsCacheExpiredReturnsNotExpired is a silly one, you typed the unit test correctly in the video, but in the code the source got changed to:

@Test
fun isProjectsCacheExpiredReturnsNotExpired() {
cache.setLastCacheTime(1000L).test()
val testObserver = cache.isProjectsCacheExpired().test()
testObserver.assertValue(false)
}

which is not correct.

Got same errors as Steve:

1) As per example code In isProjectsCacheExpiredReturnsNotExpired() test there is a cache.setLastCacheTime(1000L).test() but in a lesson we use cache.setLastCacheTime(System.currentTimeMillis()).test()

2) ProjectEntity has to be data class instead of class

3) In makeProjectEntity() had to change DataFactory.randomBoolean() to false to make getBookmarkedProjectsReturnsData() works

Got a strange error in isProjectsCacheExpired tests

java.lang.AssertionError: Expected: true (class: Boolean), Actual: []

Seems like cache.isProjectsCacheExpired returns a Single with an empty array, not with true or false.

In RxRoom, Flowable will always emit at least one item, so using single() and isEmpty() functions won't work as expected.

So, the one where you are checking if projects are cached or not using isEmpty() function, RxRoom will still emit one item which would be an empty list object. This happens because even if no values exist in the database, RxRoom will still create an empty list first and then try to populate it if any items exist. Finally, it would emit the list object even if it is empty.

You can resolve this issue by updating the areProjectsCached() function in ProjectsCacheImpl.kt like this:


override fun areProjectsCached(): Single<Boolean> =
projectsDatabase.cachedProjectsDao()
.getProjects()
.first(emptyList())
.map { !it.isEmpty() }

Also, in case of expiration check, you are converting Flowable to Single using single(defaultItem) function. Here, RxRoom will check if any data is there or not for that query and if there is no data it would return null. Then, it would filter out the null values internally as RxJava isn't cool with null values. And eventually they will still emit an empty object, i.e., new Object() even if there is no data to emit. Thus, even if we pass defaultItem to single(defaultItem) function, it won't accept that defaultItem as the Flowable would already have java's Object instance as an existing item. So, if you don't save any data in config table before accessing it, it won't use the defaultItem and would fail.

This can be resolved if you use Maybe instead of using Flowable for getting the config as RxRoom doesn't modify the creation of Maybe internally like it does in case of Flowable.

So, in ConfigDao.kt, update getConfig() function to return Config wrapped inside a Maybe like this:


abstract fun getConfig(): Maybe<Config>

Also, change the isProjectsCacheExpired() function in ProjectsCacheImpl.kt like this:


projectsDatabase.configDao()
.getConfig()
.toSingle(Config(lastCacheTime = 0L))
.map {
System.currentTimeMillis() - it.lastCacheTime > (60 * 10 * 1000).toLong()
}

This might resolve the issues people are facing in this thread regarding the failure of expiration tests.

NOTE: You can also check createFlowable method in RxRoom.java on how it is created internally.

You can read the comment here also as Caster.io's markdown formatting isn't that nice: https://gist.github.com/crazyhitty/950b52a7c1bb5ddeaba1814e20aeab6e

Thanks for sharing your thoughts here :)

A few things:

  • Flowable will not always emit an item when it's used, if there is no row in the database when the query is made then there will be no emission. That means that in this case, if there is no config that is stored then no emission will take place. There are some great notes on Room here: https://medium.com/androiddevelopers/room-rxjava-acb0cd4f3757

  • You could use a Maybe here also, but it wouldn't work any differently than it currently is. If there is no row present where Maybe is used then the maybe will Complete - if no row is present when Single is used then an error will be thrown, so handling onError for a single and onComplete for a Maybe. The only different is that for Maybe, onComplete is called when there is no value and when there is a value, however when there is a value onSuccess is also called. So because of this I feel like using a Single makes it much easier to follow :)

I also experienced the same issue. I get an empty list instead of no emission, so it does not default to the Cache(1000L) defined.

Hi @Joe Birch and @WhomeverSeeingThisComment. Why are we not mocking entitymapper in these tests like the code below ? Is there any specific reason . Is it unnecessary ? Or am I missing something obvious.

private var entityMapper = mock "&lt" CachedProjectMapper "&gt" ()

private fun stubProjectMappersMapperMapToCache(model: CachedProject, entity: ProjectEntity) {
whenever(entityMapper.mapToCache(entity)).thenReturn(model)
}

@Test
fun saveProjetsCompletes() {
val projects = listOf(ProjectDataFactory.makeProjectEntity())
projects.forEach { stubProjectMappersMapperMapToCache(ProjectDataFactory.makeCachedProject(), it) }
val testObserver = cache.saveProjects(projects).test()
testObserver.assertComplete()
}

It would be best practice here to mock the mapper class, this is because this test doesn't really care about what the mapper produces (that's not what we're testing here). I kind of just wanted to keep the test simple for demonstration purposes - but I can update this to reflect the best practice which we should follow :)

Hello Joe.

When I try to run "isProjectsCacheExpiredReturnsExpired()" I got the following error:

"java.lang.AssertionError: Expected: true (class: Boolean), Actual: "

That's my code for this "isProjectsCacheExpiredReturnsExpired()":


@Test
fun isProjectsCacheExpiredReturnsExpired() {
val testObserver = cache.isProjectsCacheExpired().test()
testObserver.assertValue(true)
}

Also, when I try to run "isProjectsCacheExpiredReturnsNotExpired" I got the following error:

"java.lang.AssertionError: Expected: false (class: Boolean), Actual: "

That's my code for this "isProjectsCacheExpiredReturnsNotExpired()":


@Test
fun isProjectsCacheExpiredReturnsNotExpired() {
cache.setLastCacheTime(System.currentTimeMillis()).test()
val testObserver = cache.isProjectsCacheExpired().test()
testObserver.assertValue(false)
}

Any tips here?

Hey Rodrigo,

It looks like this test is actually a little flakey. It was passing from my side fine, but i've managed to see this failure also since. There is a solution on this GH issue (https://github.com/hitherejoe/GithubTrending/issues/5#issuecomment-438115059) - I'm yet to update the video, but would you be up for giving the code there a try and let me know if it works for you? :) Thanks!

Hello Joe.

It remains with error for the test of test method isProjectsCacheExpiredReturnsNotExpired:

```
"/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java" -ea -Didea.test.cyclic.buffer.size=1048576 -Didea.launcher.port=50193 ...
WARNING: No manifest file found at ./AndroidManifest.xml.
Falling back to the Android OS resources only.
To remove this warning, annotate your test class with @Config(manifest=Config.NONE).
java.lang.AssertionError: Expected: false (class: Boolean), Actual:

at io.reactivex.observers.BaseTestConsumer.fail(BaseTestConsumer.java:162)
at io.reactivex.observers.BaseTestConsumer.assertValue(BaseTestConsumer.java:327)
at br.cericatto.cache.ProjectsCacheImplTest.isProjectsCacheExpiredReturnsNotExpired(ProjectsCacheImplTest.kt:104)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.robolectric.RobolectricTestRunner$HelperTestRunner$1.evaluate(RobolectricTestRunner.java:497)
at org.junit.rules.TestWatcher$1.evaluate(TestWatcher.java:55)
at org.junit.rules.RunRules.evaluate(RunRules.java:20)
at org.robolectric.internal.SandboxTestRunner$2.evaluate(SandboxTestRunner.java:228)
at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:110)
at org.robolectric.internal.SandboxTestRunner.runChild(SandboxTestRunner.java:37)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.robolectric.internal.SandboxTestRunner$1.evaluate(SandboxTestRunner.java:64)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMainV2.main(AppMainV2.java:131)

Process finished with exit code 255
```

Also the same error happens for this other test method isProjectsCacheExpiredReturnsExpired.

Sorry about that - I haven't seen this error in this class myself! Would you be up for copying your test class into a gist so that I can try and reproduce it?

Joe, here it is the Gist:
https://gist.github.com/graffiti75/2f4c7d81728ec7fc297f242cc137fb51

>
You need to go PRO to post comments.

Lessons in Android Clean Architecture

Introduction to Clean Architecture
03:40
Joe Birch
GDE
Android and Clean Architecture
03:51
Joe Birch
GDE
Setting up a Multi-Module project
02:24
Joe Birch
GDE
Setting up the Domain layer module
02:24
Joe Birch
GDE
Creating the abstraction for our RxJava Observation Thread
02:02
Joe Birch
GDE
Setting up the Business Data for the Domain Layer
01:47
Joe Birch
GDE
Setting up the data access interface for the Domain Layer
01:49
Joe Birch
GDE
Setting up the Domain Layer Interactor classes
Pro
03:28
Joe Birch
GDE
Implementing the Get Projects use case
Pro
01:49
Joe Birch
GDE
Implementing the get Bookmarked Projects use case
Pro
01:33
Joe Birch
GDE
Implementing the Bookmark Project use case
Pro
01:57
Joe Birch
GDE
Implementing the UnBookmark Project use case (Domain layer 8)
Pro
02:03
Joe Birch
GDE
Testing the Get Projects use case
Pro
02:41
Joe Birch
GDE
Testing the Get Bookmarked Projects use case
Pro
02:04
Joe Birch
GDE
Testing the Bookmark Project use case
Pro
02:10
Joe Birch
GDE
Testing the UnBookmark Project use case
Pro
02:06
Joe Birch
GDE
Setting up the data module
Pro
02:17
Joe Birch
GDE
Creating the data models for the data layer
Pro
02:48
Joe Birch
GDE
Creating the external source interfaces for our data layer
Pro
02:24
Joe Birch
GDE
Creating the data layer store interface
Pro
01:19
Joe Birch
GDE
Creating the data layer cache data store
Pro
01:21
Joe Birch
GDE
Creating the data layer remote data store
Pro
01:39
Joe Birch
GDE
Creating the data store factory class
Pro
01:24
Joe Birch
GDE
Creating the central access point for the data layer
Pro
03:00
Joe Birch
GDE
Setting up the tests for the Data Layer
Pro
00:46
Joe Birch
GDE
Testing the data store factory class
Pro
01:39
Joe Birch
GDE
Testing the project entity mapper
Pro
01:18
Joe Birch
GDE
Testing the projects data repository
Pro
06:02
Joe Birch
GDE
Testing the projects cache data store
Pro
06:30
Joe Birch
GDE
Testing the projects remote data store
Pro
02:17
Joe Birch
GDE
Setting up the remote module
Pro
02:21
Joe Birch
GDE
Creating the data models for the remote layer
Pro
02:06
Joe Birch
GDE
Creating the Retrofit Service for the remote data source
Pro
01:58
Joe Birch
GDE
Creating the Retrofit Service Factory for the remote service interface
Pro
02:52
Joe Birch
GDE
Creating the data model mapper for the Remote Layer
Pro
01:59
Joe Birch
GDE
Creating the Remote Source implementation
Pro
01:55
Joe Birch
GDE
Setting up the Remote layer for testing
Pro
02:13
Joe Birch
GDE
Testing the remote model mapper class
Pro
01:39
Joe Birch
GDE
Testing the remote source implementation
Pro
05:09
Joe Birch
GDE
Setting up the Cache module
Pro
02:45
Joe Birch
GDE
Setting up the Cache layer data models
Pro
02:02
Joe Birch
GDE
Creating the database for the cache layer
Pro
01:34
Joe Birch
GDE
Creating the cached project Data Access Object
Pro
02:42
Joe Birch
GDE
Creating the Configuration Data Access Object
Pro
02:07
Joe Birch
GDE
Creating the cache model mapper
Pro
02:06
Joe Birch
GDE
Creating the Projects Cache data source Implementation
Pro
03:57
Joe Birch
GDE
Setting up the tests for our cache layer
Pro
01:17
Joe Birch
GDE
Testing the cached project model mapper
Pro
01:44
Joe Birch
GDE
Testing the Cached project DAO
Pro
04:33
Joe Birch
GDE
Testing the Configuration DAO
Pro
02:05
Joe Birch
GDE
Testing the projects cache implementation
Pro
05:48
Joe Birch
GDE
Setting up the presentation module
Pro
02:05
Joe Birch
GDE
Creating the data models for the presentation layer
Pro
01:13
Joe Birch
GDE
Creating the data model mapper for the presentation layer
Pro
01:26
Joe Birch
GDE
Creating the presentation state model
Pro
01:39
Joe Birch
GDE
Creating the browse projects view model
Pro
04:25
Joe Birch
GDE
Creating the bookmarked projects view model
Pro
03:05
Joe Birch
GDE
Setting up the presentation module tests
Pro
02:09
Joe Birch
GDE
Testing the view model mapper
Pro
01:17
Joe Birch
GDE
Testing our Browse Projects View Model implementation
Pro
04:36
Joe Birch
GDE
Testing our Browse Bookmarked Projects View Model implementation
Pro
04:34
Joe Birch
GDE
Setting up the UI module
Pro
02:22
Joe Birch
GDE
Creating the Post Execution Thread Implementation
Pro
01:26
Joe Birch
GDE
Setting up the application class
Pro
00:50
Joe Birch
GDE
Setting up the Dependency Injection for the UI module
Pro
02:46
Joe Birch
GDE
Creating the UI modules Project representation
Pro
00:50
Joe Birch
GDE
Creating the the Project model mapper for the UI module
Pro
01:24
Joe Birch
GDE
Setting up the Browse Projects Activity
Pro
01:08
Joe Birch
GDE
Configuring the DI for the project
Pro
04:37
Joe Birch
GDE
Creating the Project item view
Pro
01:51
Joe Birch
GDE
Creating the Browse Projects Adapter
Pro
03:27
Joe Birch
GDE
Creating the browse bookmarked projects adapter
Pro
03:54
Joe Birch
GDE
Creating the browse bookmarked projects activity
Pro
03:18
Joe Birch
GDE
Tying together the browse projects and bookmarked projects activity
Pro
02:30
Joe Birch
GDE
Setting up Unit Tests for the UI module
Pro
00:54
Joe Birch
GDE
Test the project model mapper
Pro
01:10
Joe Birch
GDE
Configuring the Dependency Injection for our User Interface tests
Pro
06:10
Joe Birch
GDE
Writing Espresso tests for the Browse Projects Activity
Pro
03:15
Joe Birch
GDE
Writing Espresso tests for the Bookmarked Projects Activity
Pro
02:34
Joe Birch
GDE
Simplifying the work we have done
Pro
05:14
Joe Birch
GDE