@@ -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```
2122object AndroidVersion {
@@ -209,7 +210,7 @@ And navigation folder should contain navigation graph with
209210
210211There 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
2132142 . ``` graphResName``` is the name of the navigation folder which is nav_graph_gallery.xml for this boilerplate
2142153. 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+ ```
0 commit comments