Skip to content

Commit a4cd4fb

Browse files
Update README.md
1 parent ffac35e commit a4cd4fb

2 files changed

Lines changed: 339 additions & 19 deletions

File tree

README.md

Lines changed: 339 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ to create a requirements when starting a new project.
1313
* Dagger Hilt, Dynamic Feature Modules with Navigation Components, ViewModel, Retrofit, Room, RxJava, Coroutines libraries adn dependencies are set up.
1414
* ```features``` and ```libraries``` folders are used to include android libraries and dynamic feature modules
1515
* In core module dagger hilt dependencies and ```@EntryPoint``` is created
16+
* test-utils module for shared folder for tes and androidTest folders, LiveDataObserver and FlowObserver.
1617

17-
###Note
18-
Change ```applicationId```in ```Version.AndroidVersion``` first
18+
### Note
19+
Change ```applicationId```in ```Version.AndroidVersion```
1920

2021
```
2122
object AndroidVersion {
@@ -209,7 +210,7 @@ And navigation folder should contain navigation graph with
209210

210211
There are 3 important properties that should be carefully added to main graph for not receiving error
211212

212-
1. id of the navigation,``` android:id="@+id/nav_graph_gallery"``, should be same with the dynamic feature id
213+
1. id of the navigation,``` android:id="@+id/nav_graph_gallery"```, should be same with the dynamic feature id
213214
2. ```graphResName``` is the name of the navigation folder which is nav_graph_gallery.xml for this boilerplate
214215
3. module name should be exactly same name dynamic feature module is named.
215216

@@ -325,3 +326,338 @@ And creat this component in a ```Fragment``` or ```Activity``` using
325326
```
326327
327328
🔥 ```EntryPointAccessors.fromApplication``` depends on which component ```CoreModule``` uses ```@InstallIn``` with
329+
330+
## Testing
331+
332+
333+
### test-utils
334+
```test-shared``` folder contains common rules, and utilities for both ```test``` and ```androidTest``` for using
335+
both with unit tests and integration test.
336+
337+
```
338+
sourceSets {
339+
340+
val sharedTestDir = "src/test-shared/java"
341+
342+
getByName("test") {
343+
java.srcDir(sharedTestDir)
344+
}
345+
346+
getByName("androidTest") {
347+
java.srcDir(sharedTestDir)
348+
resources.srcDir("src/test/resources")
349+
}
350+
}
351+
```
352+
353+
### LiveDataObserver
354+
355+
This class is observer for testing ```LiveData``` that emits more than one values and similar to RxJava ```TestObserver```.
356+
357+
```
358+
class LiveDataTestObserver<T> constructor(
359+
private val liveData: LiveData<T>
360+
) : Observer<T> {
361+
362+
init {
363+
liveData.observeForever(this)
364+
}
365+
366+
private val testValues = mutableListOf<T>()
367+
368+
override fun onChanged(t: T) {
369+
if (t != null) testValues.add(t)
370+
}
371+
372+
fun assertNoValues(): LiveDataTestObserver<T> {
373+
if (testValues.isNotEmpty()) throw AssertionError(
374+
"Assertion error with actual size ${testValues.size}"
375+
)
376+
return this
377+
}
378+
379+
fun assertValueCount(count: Int): LiveDataTestObserver<T> {
380+
if (count < 0) throw AssertionError(
381+
"Assertion error! value count cannot be smaller than zero"
382+
)
383+
if (count != testValues.size) throw AssertionError(
384+
"Assertion error! with expected $count while actual ${testValues.size}"
385+
)
386+
return this
387+
}
388+
389+
fun assertValues(vararg predicates: T): LiveDataTestObserver<T> {
390+
if (!testValues.containsAll(predicates.asList())) throw AssertionError("Assertion error!")
391+
return this
392+
}
393+
394+
fun assertValues(predicate: (List<T>) -> Boolean): LiveDataTestObserver<T> {
395+
predicate(testValues)
396+
return this
397+
}
398+
399+
fun values(predicate: (List<T>) -> Unit): LiveDataTestObserver<T> {
400+
predicate(testValues)
401+
return this
402+
}
403+
404+
fun values(): List<T> {
405+
return testValues
406+
}
407+
408+
/**
409+
* Removes this observer from the [LiveData] which was observing
410+
*/
411+
fun dispose() {
412+
liveData.removeObserver(this)
413+
}
414+
415+
/**
416+
* Clears data available in this observer and removes this observer from the [LiveData] which was observing
417+
*/
418+
fun clear() {
419+
testValues.clear()
420+
dispose()
421+
}
422+
}
423+
424+
fun <T> LiveData<T>.test(): LiveDataTestObserver<T> {
425+
426+
val testObserver = LiveDataTestObserver(this)
427+
428+
// Remove this testObserver that is added in init block of TestObserver, and clears previous data
429+
testObserver.clear()
430+
observeForever(testObserver)
431+
432+
return testObserver
433+
}
434+
```
435+
436+
### FlowTestObserver
437+
438+
TestObserver with declarative assertion methods to test more than multiple states and values sequentaially.
439+
440+
```
441+
class FlowTestObserver<T>(
442+
private val coroutineScope: CoroutineScope,
443+
private val flow: Flow<T>,
444+
private val waitForDelay: Boolean = false
445+
) {
446+
private val testValues = mutableListOf<T>()
447+
private var error: Throwable? = null
448+
449+
private var isInitialized = false
450+
451+
private var isCompleted = false
452+
453+
private lateinit var job: Job
454+
455+
456+
private suspend fun initializeAndJoin() {
457+
job = createJob(coroutineScope)
458+
}
459+
460+
461+
private suspend fun initialize() {
462+
463+
if (!isInitialized) {
464+
isInitialized = true
465+
466+
if (waitForDelay) {
467+
try {
468+
withTimeout(Long.MAX_VALUE) {
469+
job = createJob(this)
470+
}
471+
} catch (e: Exception) {
472+
isCompleted = false
473+
}
474+
} else {
475+
initializeAndJoin()
476+
}
477+
}
478+
}
479+
480+
private fun createJob(scope: CoroutineScope): Job {
481+
482+
val job = flow
483+
.onStart {
484+
}
485+
.onCompletion {
486+
isCompleted = true
487+
}
488+
.catch { throwable ->
489+
error = throwable
490+
}
491+
.onEach {
492+
testValues.add(it)
493+
}
494+
.launchIn(scope)
495+
496+
return job
497+
}
498+
499+
500+
suspend fun assertNoValues(): FlowTestObserver<T> {
501+
initialize()
502+
if (testValues.isNotEmpty()) throw AssertionError(
503+
"Assertion error! Actual size ${testValues.size}"
504+
)
505+
return this
506+
}
507+
508+
suspend fun assertValueCount(count: Int): FlowTestObserver<T> {
509+
initialize()
510+
if (count < 0) throw AssertionError(
511+
"Assertion error! Value count cannot be smaller than zero"
512+
)
513+
if (count != testValues.size) throw AssertionError(
514+
"Assertion error! Expected $count while actual ${testValues.size}"
515+
)
516+
return this
517+
}
518+
519+
suspend fun assertValues(vararg values: T): FlowTestObserver<T> {
520+
initialize()
521+
if (!testValues.containsAll(values.asList()))
522+
throw AssertionError("Assertion error! At least one value does not match")
523+
return this
524+
}
525+
526+
suspend fun assertValues(predicate: (List<T>) -> Boolean): FlowTestObserver<T> {
527+
528+
initialize()
529+
530+
if (!predicate(testValues))
531+
throw AssertionError("Assertion error! At least one value does not match")
532+
return this
533+
}
534+
535+
suspend fun assertError(throwable: Throwable): FlowTestObserver<T> {
536+
537+
initialize()
538+
539+
val errorNotNull = exceptionNotNull()
540+
541+
if (!(errorNotNull::class.java == throwable::class.java &&
542+
errorNotNull.message == throwable.message)
543+
)
544+
throw AssertionError("Assertion Error! throwable: $throwable does not match $errorNotNull")
545+
return this
546+
}
547+
548+
suspend fun assertError(errorClass: Class<Throwable>): FlowTestObserver<T> {
549+
550+
initialize()
551+
552+
val errorNotNull = exceptionNotNull()
553+
554+
if (errorNotNull::class.java != errorClass)
555+
throw AssertionError("Assertion Error! errorClass $errorClass does not match ${errorNotNull::class.java}")
556+
return this
557+
}
558+
559+
suspend fun assertError(predicate: (Throwable) -> Boolean): FlowTestObserver<T> {
560+
561+
initialize()
562+
563+
val errorNotNull = exceptionNotNull()
564+
565+
if (!predicate(errorNotNull))
566+
throw AssertionError("Assertion Error! Exception for $errorNotNull")
567+
return this
568+
}
569+
570+
suspend fun assertNoError(): FlowTestObserver<T> {
571+
572+
initialize()
573+
574+
if (error != null)
575+
throw AssertionError("Assertion Error! Exception occurred $error")
576+
577+
return this
578+
}
579+
580+
suspend fun assertNull(): FlowTestObserver<T> {
581+
582+
initialize()
583+
584+
testValues.forEach {
585+
if (it != null) throw AssertionError("Assertion Error! There are more than one item that is not null")
586+
}
587+
588+
return this
589+
}
590+
591+
suspend fun assertComplete(): FlowTestObserver<T> {
592+
593+
initialize()
594+
595+
if (!isCompleted) throw AssertionError("Assertion Error! Job is not completed yet!")
596+
return this
597+
}
598+
599+
suspend fun assertNotComplete(): FlowTestObserver<T> {
600+
601+
initialize()
602+
603+
if (isCompleted) throw AssertionError("Assertion Error! Job is completed!")
604+
return this
605+
}
606+
607+
suspend fun values(predicate: (List<T>) -> Unit): FlowTestObserver<T> {
608+
predicate(testValues)
609+
return this
610+
}
611+
612+
suspend fun values(): List<T> {
613+
614+
initialize()
615+
616+
return testValues
617+
}
618+
619+
620+
private fun exceptionNotNull(): Throwable {
621+
622+
if (error == null)
623+
throw AssertionError("There is no exception")
624+
625+
return error!!
626+
}
627+
628+
fun dispose() {
629+
job.cancel()
630+
}
631+
}
632+
633+
/**
634+
* Creates a RxJava2 style test observer that uses `onStart`, `onEach`, `onCompletion`
635+
*
636+
* * Set waitForDelay true for testing delay.
637+
*
638+
* ### Note: waiting for delay with a channel that sends values throw TimeoutCancellationException, don't use timeout with channel
639+
* TODO Fix channel issue
640+
*/
641+
suspend fun <T> Flow<T>.test(
642+
scope: CoroutineScope,
643+
waitForDelay: Boolean = false
644+
): FlowTestObserver<T> {
645+
return FlowTestObserver(scope, this@test, waitForDelay)
646+
}
647+
648+
/**
649+
* Test function that awaits with time out until each delay method is run and then since
650+
* it takes a predicate that runs after a timeout.
651+
*/
652+
suspend fun <T> Flow<T>.testAfterDelay(
653+
scope: CoroutineScope,
654+
predicate: suspend FlowTestObserver<T>.() -> Unit
655+
656+
): Job {
657+
return scope.launch(coroutineContext) {
658+
FlowTestObserver(this, this@testAfterDelay, true).predicate()
659+
}
660+
}
661+
662+
663+
```

libraries/test-utils/src/test-shared/java/com/smarttoolfactory/test_utils/util/LiveDataTestUtil.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,6 @@ fun <T> LiveData<T>.getOrAwaitValue(
5252
* * Use with `InstantTaskExecutorRule` or a similar mechanism to execute tasks synchronously.
5353
*
5454
*/
55-
56-
/*
57-
TODO Add observe forever and timeout mechanism
58-
59-
Hint: Make other functions wait while onChanged is still running
60-
*/
6155
class LiveDataTestObserver<T> constructor(
6256
private val liveData: LiveData<T>
6357
) : Observer<T> {
@@ -94,16 +88,6 @@ class LiveDataTestObserver<T> constructor(
9488
return this
9589
}
9690

97-
// fun assertValues(predicate: List<T>.() -> Boolean): TestObserver<T> {
98-
// testValues.predicate()
99-
// return this
100-
// }
101-
//
102-
// fun values(predicate: List<T>.() -> Unit): TestObserver<T> {
103-
// testValues.predicate()
104-
// return this
105-
// }
106-
10791
fun assertValues(predicate: (List<T>) -> Boolean): LiveDataTestObserver<T> {
10892
predicate(testValues)
10993
return this

0 commit comments

Comments
 (0)