Skip to content

Commit d4fb9a7

Browse files
authored
feat: ✅ create parametric repository test suite (#30)
close #12
1 parent a858ac0 commit d4fb9a7

File tree

6 files changed

+271
-1
lines changed

6 files changed

+271
-1
lines changed

app/build.gradle

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ android {
3535
enabled = true
3636
}
3737

38+
compileOptions {
39+
sourceCompatibility = JavaVersion.VERSION_1_8
40+
targetCompatibility = JavaVersion.VERSION_1_8
41+
}
42+
3843
androidExtensions {
3944
experimental = true
4045
}
@@ -46,6 +51,11 @@ kapt {
4651
}
4752
}
4853

54+
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
55+
kotlinOptions {
56+
jvmTarget = JavaVersion.VERSION_1_8
57+
}
58+
}
4959

5060
dependencies {
5161
kapt libs.databinding_compiler
@@ -83,6 +93,7 @@ dependencies {
8393
exclude module: 'junit'
8494
}
8595
testImplementation libs.junit_api
96+
testImplementation libs.junit_params
8697
testImplementation libs.kotlintest
8798
testImplementation (libs.koin_test) {
8899
exclude module: 'junit'

app/src/main/java/es/ffgiraldez/comicsearch/comics/data/ComicRepository.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ abstract class ComicRepository<T> (
4141
results.fold({
4242
Flowable.just(results)
4343
}, {
44-
local.insert(term, it).toFlowable<Either<ComicError, List<T>>>()
44+
local.insert(term, it).toFlowable()
4545
})
4646

4747
private fun fetch(it: Some<Query>): Flowable<Either<EmptyResultsError, List<T>>> =

app/src/test/java/es/ffgiraldez/comicsearch/comic/gen/EntitiesGen.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ class SuggestionGenerator : Gen<Either<ComicError, List<String>>> {
4040
}
4141
}
4242

43+
class SuggestionResultGenerator : Gen<List<String>> {
44+
override fun constants(): Iterable<List<String>> = emptyList()
45+
46+
override fun random(): Sequence<List<String>> = generateSequence<List<String>> {
47+
(1..10).fold(emptyList()) { acc, _ -> acc + Gen.query().random().iterator().next() }
48+
}
49+
}
50+
4351
class SuggestionViewStateGenerator : Gen<QueryViewState<String>> {
4452
override fun constants(): Iterable<QueryViewState<String>> = listOf(
4553
QueryViewState.idle(),
@@ -67,6 +75,17 @@ class VolumeGenerator : Gen<Volume> {
6775

6876
}
6977

78+
class VolumeResultGenerator : Gen<List<Volume>> {
79+
override fun constants(): Iterable<List<Volume>> = emptyList()
80+
81+
override fun random(): Sequence<List<Volume>> = generateSequence<List<Volume>> {
82+
(1..10).fold(emptyList()) { acc, _ ->
83+
acc + Gen.volume().random().iterator().next()
84+
}
85+
86+
}
87+
}
88+
7089
class SearchGenerator : Gen<Either<ComicError, List<Volume>>> {
7190
override fun constants(): Iterable<Either<ComicError, List<Volume>>> = emptyList()
7291

@@ -100,6 +119,8 @@ class SearchViewStateGenerator : Gen<QueryViewState<Volume>> {
100119

101120
fun Gen.Companion.suggestions(): Gen<Either<ComicError, List<String>>> = SuggestionGenerator()
102121

122+
fun Gen.Companion.suggestionList(): Gen<List<String>> = SuggestionResultGenerator()
123+
103124
fun Gen.Companion.suggestionsViewState(): Gen<QueryViewState<String>> = SuggestionViewStateGenerator()
104125

105126
fun Gen.Companion.suggestionsErrorViewState(): Gen<QueryViewState.Error> = suggestionsViewState().filterIsInstance()
@@ -114,6 +135,8 @@ fun Gen.Companion.query(): Gen<String> = QueryGenerator()
114135

115136
fun Gen.Companion.volume(): Gen<Volume> = VolumeGenerator()
116137

138+
fun Gen.Companion.volumeList(): Gen<List<Volume>> = VolumeResultGenerator()
139+
117140
fun Gen.Companion.searchViewState(): Gen<QueryViewState<Volume>> = SearchViewStateGenerator()
118141

119142
fun Gen.Companion.searchErrorViewState(): Gen<QueryViewState.Error> = searchViewState().filterIsInstance()
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package es.ffgiraldez.comicsearch.comics.data
2+
3+
import com.nhaarman.mockitokotlin2.any
4+
import com.nhaarman.mockitokotlin2.verify
5+
import es.ffgiraldez.comicsearch.comics.data.GivenComicRepository.Companion.expectedTerm
6+
import es.ffgiraldez.comicsearch.comics.data.GivenComicRepository.Companion.givenComicRepositoryMethodSource
7+
import es.ffgiraldez.comicsearch.comics.domain.ComicError.EmptyResultsError
8+
import es.ffgiraldez.comicsearch.comics.domain.ComicError.NetworkError
9+
import es.ffgiraldez.comicsearch.platform.left
10+
import es.ffgiraldez.comicsearch.platform.right
11+
import io.kotlintest.properties.Gen
12+
import org.junit.jupiter.params.ParameterizedTest
13+
import org.junit.jupiter.params.provider.MethodSource
14+
15+
class ComicRepositoryShould
16+
: GivenComicRepository by givenComicRepository {
17+
18+
@ParameterizedTest
19+
@MethodSource(givenComicRepositoryMethodSource)
20+
fun <T> `return EmptyResultsError when local query does not have results`(
21+
localDataSource: ComicLocalDataSource<T>,
22+
remoteDataSource: ComicRemoteDataSource<T>,
23+
generator: Gen<List<T>>,
24+
repository: ComicRepository<T>
25+
) {
26+
localDataSource.withValues(term = expectedTerm, results = emptyList())
27+
28+
val observer = repository.findByTerm(expectedTerm).test()
29+
30+
observer.assertValue(left(EmptyResultsError))
31+
}
32+
33+
@ParameterizedTest
34+
@MethodSource(givenComicRepositoryMethodSource)
35+
fun <T> `return results list when local query does have results`(
36+
localDataSource: ComicLocalDataSource<T>,
37+
remoteDataSource: ComicRemoteDataSource<T>,
38+
generator: Gen<List<T>>,
39+
repository: ComicRepository<T>
40+
) {
41+
val expectedList = generator.random().first()
42+
localDataSource.withValues(term = expectedTerm, results = expectedList)
43+
44+
val observer = repository.findByTerm(expectedTerm).test()
45+
46+
observer.assertValue(right(expectedList))
47+
}
48+
49+
@ParameterizedTest
50+
@MethodSource(givenComicRepositoryMethodSource)
51+
fun <T> `ask for remote result when does not have local query`(
52+
localDataSource: ComicLocalDataSource<T>,
53+
remoteDataSource: ComicRemoteDataSource<T>,
54+
generator: Gen<List<T>>,
55+
repository: ComicRepository<T>
56+
) {
57+
localDataSource.withoutValues()
58+
remoteDataSource.withoutValues()
59+
60+
repository.findByTerm(expectedTerm).test()
61+
62+
verify(remoteDataSource).findByTerm(any())
63+
64+
}
65+
66+
@ParameterizedTest
67+
@MethodSource(givenComicRepositoryMethodSource)
68+
fun <T> `safe remote result on local when does not have local query`(
69+
localDataSource: ComicLocalDataSource<T>,
70+
remoteDataSource: ComicRemoteDataSource<T>,
71+
generator: Gen<List<T>>,
72+
repository: ComicRepository<T>
73+
) {
74+
val expected = generator.random().first()
75+
localDataSource.withoutValues()
76+
remoteDataSource.withValues(results = expected)
77+
78+
repository.findByTerm(expectedTerm).test()
79+
80+
verify(localDataSource).insert(expectedTerm, expected)
81+
}
82+
83+
@ParameterizedTest
84+
@MethodSource(givenComicRepositoryMethodSource)
85+
fun <T> `return EmptyResultError when local and remote does not have results`(
86+
localDataSource: ComicLocalDataSource<T>,
87+
remoteDataSource: ComicRemoteDataSource<T>,
88+
generator: Gen<List<T>>,
89+
repository: ComicRepository<T>
90+
) {
91+
localDataSource
92+
.withoutValues()
93+
.withSave()
94+
remoteDataSource.withoutValues()
95+
96+
val observer = repository.findByTerm(expectedTerm).test()
97+
98+
observer.assertValue(left(EmptyResultsError))
99+
}
100+
101+
@ParameterizedTest
102+
@MethodSource(givenComicRepositoryMethodSource)
103+
fun <T> `return NetworkError when local does not have results and remote fails`(
104+
localDataSource: ComicLocalDataSource<T>,
105+
remoteDataSource: ComicRemoteDataSource<T>,
106+
generator: Gen<List<T>>,
107+
repository: ComicRepository<T>
108+
) {
109+
localDataSource.withoutValues()
110+
remoteDataSource.withError()
111+
112+
val observer = repository.findByTerm(expectedTerm).test()
113+
114+
observer.assertValue(left(NetworkError))
115+
}
116+
117+
@ParameterizedTest
118+
@MethodSource(givenComicRepositoryMethodSource)
119+
fun <T> `return results list when local does not have results but remote have results`(
120+
localDataSource: ComicLocalDataSource<T>,
121+
remoteDataSource: ComicRemoteDataSource<T>,
122+
generator: Gen<List<T>>,
123+
repository: ComicRepository<T>
124+
) {
125+
val expected = generator.random().first()
126+
localDataSource.withoutValues()
127+
.withSave()
128+
remoteDataSource.withValues(results = expected)
129+
130+
val observer = repository.findByTerm(expectedTerm).test()
131+
132+
observer.assertValue(right(expected))
133+
}
134+
}
135+
136+
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package es.ffgiraldez.comicsearch.comics.data
2+
3+
import arrow.core.Option
4+
import arrow.core.toOption
5+
import com.nhaarman.mockitokotlin2.any
6+
import com.nhaarman.mockitokotlin2.mock
7+
import com.nhaarman.mockitokotlin2.whenever
8+
import es.ffgiraldez.comicsearch.comic.gen.suggestionList
9+
import es.ffgiraldez.comicsearch.comic.gen.volumeList
10+
import es.ffgiraldez.comicsearch.comics.domain.Query
11+
import es.ffgiraldez.comicsearch.query.search.data.SearchRepository
12+
import es.ffgiraldez.comicsearch.query.sugestion.data.SuggestionRepository
13+
import io.kotlintest.properties.Gen
14+
import io.reactivex.Completable
15+
import io.reactivex.Flowable
16+
import io.reactivex.Single
17+
import io.reactivex.processors.BehaviorProcessor
18+
import io.reactivex.processors.FlowableProcessor
19+
import io.reactivex.processors.PublishProcessor
20+
import io.reactivex.processors.ReplayProcessor
21+
import io.reactivex.subjects.PublishSubject
22+
import io.reactivex.subjects.ReplaySubject
23+
import io.reactivex.subjects.Subject
24+
import org.junit.jupiter.params.provider.Arguments
25+
import java.util.stream.Stream
26+
27+
28+
val givenComicRepository = object : GivenComicRepository {
29+
override var localQuery: FlowableProcessor<Option<Query>> = BehaviorProcessor.create()
30+
private val localResults = BehaviorProcessor.create<Any>()
31+
32+
33+
override fun <T> localResults(): FlowableProcessor<List<T>> = localResults as FlowableProcessor<List<T>>
34+
35+
}
36+
37+
interface GivenComicRepository {
38+
fun <T> ComicLocalDataSource<T>.withoutValues(): ComicLocalDataSource<T> = apply {
39+
withValues()
40+
}
41+
42+
fun <T> ComicLocalDataSource<T>.withSave(): ComicLocalDataSource<T> = apply {
43+
whenever(insert(any(), any())).thenAnswer {
44+
val query = it.arguments[0] as String
45+
val results = it.arguments[1] as List<T>
46+
Completable.complete().apply {
47+
localResults<T>().onNext(results)
48+
localQuery.onNext(Query(4, query).toOption())
49+
}
50+
}
51+
}
52+
53+
fun <T> ComicLocalDataSource<T>.withValues(term: String? = null, results: List<T> = emptyList()): ComicLocalDataSource<T> = apply {
54+
configure()
55+
val q = term?.let { Query(3, term) }
56+
localQuery.onNext(q.toOption())
57+
q?.let {
58+
localResults<T>().onNext(results)
59+
}
60+
}
61+
62+
fun <T> ComicRemoteDataSource<T>.withoutValues(): ComicRemoteDataSource<T> = apply {
63+
withValues()
64+
}
65+
66+
fun <T> ComicRemoteDataSource<T>.withError(): ComicRemoteDataSource<T> = apply {
67+
whenever(findByTerm(any())).thenReturn(Single.error{ RuntimeException("BOOM!")})
68+
}
69+
70+
fun <T> ComicRemoteDataSource<T>.withValues(results: List<T> = emptyList()): ComicRemoteDataSource<T> = apply {
71+
whenever(findByTerm(any())).thenReturn(Single.just(results))
72+
}
73+
74+
var localQuery: FlowableProcessor<Option<Query>>
75+
fun <T> localResults(): FlowableProcessor<List<T>>
76+
77+
private fun <T> ComicLocalDataSource<T>.configure() {
78+
localQuery = BehaviorProcessor.create()
79+
whenever(findQueryByTerm(any())).thenReturn(localQuery)
80+
whenever(findByQuery(any())).thenReturn(localResults())
81+
}
82+
83+
84+
companion object {
85+
const val givenComicRepositoryMethodSource: String = "es.ffgiraldez.comicsearch.comics.data.GivenComicRepository#arguments"
86+
const val expectedTerm = "Batman"
87+
88+
@JvmStatic
89+
@Suppress("unused")
90+
fun arguments(): Stream<Arguments> = Stream.of(
91+
build(mock(), mock(), Gen.suggestionList(), ::SuggestionRepository),
92+
build(mock(), mock(), Gen.volumeList(), ::SearchRepository)
93+
)
94+
95+
private fun <A, B, C> build(local: A, remote: B, result: Gen<List<C>>, repo: (A, B) -> ComicRepository<C>): Arguments =
96+
Arguments.of(local, remote, result, repo(local, remote))
97+
}
98+
}
99+

dependencies.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ ext {
3838
floating_search : [group: 'com.github.arimorty', name: 'floatingsearchview', version: '2.1.1'],
3939
junit_api : [group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: versions.junit],
4040
junit_engine : [group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: versions.junit],
41+
junit_params : [group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: versions.junit],
4142
okhttp : [group: 'com.squareup.okhttp3', name: 'okhttp', version: versions.okhttp],
4243
okhttp_logging : [group: 'com.squareup.okhttp3', name: 'logging-interceptor', version: versions.okhttp],
4344
koin : [group: 'org.koin', name: 'koin-core', version: versions.koin],

0 commit comments

Comments
 (0)