Skip to content

Commit 99d4733

Browse files
author
Arkadiusz Pałka
committed
Merge remote-tracking branch 'origin/develop' into feature/MVI-2-delegation
2 parents 424d4b1 + 189374a commit 99d4733

File tree

8 files changed

+455
-11
lines changed

8 files changed

+455
-11
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ apply plugin: 'com.android.application'
22
apply plugin: 'kotlin-android'
33
apply plugin: 'kotlin-kapt'
44
apply plugin: 'kotlin-android-extensions'
5+
apply plugin: 'de.mannodermaus.android-junit5'
56

67
// Android configuration
78
android {

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ buildscript {
99
dependencies {
1010
classpath deps.build.android_gradle_plugin
1111
classpath deps.build.kotlin_gradle_plugin
12+
classpath "de.mannodermaus.gradle.plugins:android-junit5:1.3.2.0"
1213
}
1314
}
1415

dependencies.gradle

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ versions.rx_java_extensions = "0.20.10"
4141
versions.crashlytics = "2.10.1@aar"
4242
versions.joda_time = "2.10.2"
4343
versions.mapstruct = "1.3.0.Final"
44-
versions.junit = "4.12"
44+
versions.junit = "5.5.2"
4545
versions.mockito = "2.25.1"
4646
versions.mockito_ktx = "2.1.0"
4747
versions.dropwizard_testing = "1.3.13"
@@ -51,7 +51,7 @@ versions.dexmaker_mockito = "1.2"
5151
versions.androidx_runner = "1.1.2-alpha02"
5252
versions.espresso = "3.2.0-alpha02"
5353
versions.okhttp = "3.14.0"
54-
versions.robolectric = "4.0.2"
54+
versions.robolectric = "4.3"
5555
versions.glide = "4.9.0"
5656
versions.realm = "5.12.0"
5757
versions.apache_lang = "3.9"
@@ -62,6 +62,7 @@ versions.easy_validation = "1.0.1"
6262
versions.fragmentation = "1.0.1"
6363
versions.material_progress_bar = "1.6.1"
6464
versions.fast_adapter = "4.0.1"
65+
versions.mockk = "1.9.3"
6566
deps.versions = versions
6667
ext.versions = versions
6768

@@ -210,21 +211,22 @@ deps.utility = utility
210211
* Test variables.
211212
*/
212213
def test = [:]
213-
test.junit = "junit:junit:$versions.junit"
214+
test.junit_api = "org.junit.jupiter:junit-jupiter-api:$versions.junit"
215+
test.junit_params = "org.junit.jupiter:junit-jupiter-params:$versions.junit"
216+
test.junit_engine = "org.junit.jupiter:junit-jupiter-engine:$versions.junit"
214217
test.mockito = "org.mockito:mockito-core:$versions.mockito"
215218
test.mockito_ktx = "com.nhaarman.mockitokotlin2:mockito-kotlin:$versions.mockito_ktx"
216219
test.toothpick = "com.github.stephanenicolas.toothpick:toothpick-testing:$versions.toothpick_testing"
217220
test.matchers_ktx = "com.github.stephanenicolas.toothpick:toothpick-testing:$versions.toothpick_testing"
218221
test.androidx_core = "androidx.test:core:$versions.androidx_test_core"
219-
test.junit_ext_ktx = "androidx.test.ext:junit-ktx:$versions.androidx_test_core"
220-
test.junit_ext = "aandroidx.test.ext:junit:$versions.androidx_test_core"
221222
test.dexmaker_mockito = "com.google.dexmaker:dexmaker-mockito:$versions.dexmaker_mockito"
222223
test.androidx_runner = "androidx.test:runner:$versions.androidx_runner"
223224
test.androidx_rules = "androidx.test:rules:$versions.androidx_runner"
224225
test.espresso = "androidx.test.espresso:espresso-core:$versions.espresso"
225226
test.espresso_intents = "androidx.test.espresso:espresso-intents:$versions.espresso"
226227
test.mock_web_server = "com.squareup.okhttp3:mockwebserver:$versions.okhttp"
227228
test.robolectric = "org.robolectric:robolectric:$versions.robolectric"
229+
test.mockk = "io.mockk:mockk:$versions.mockk"
228230
deps.test = test
229231

230232
/**

mvi-valueadd/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ android {
2222
consumerProguardFiles 'consumer-rules.pro'
2323
}
2424

25+
testOptions {
26+
unitTests.all {
27+
useJUnitPlatform()
28+
}
29+
}
30+
2531
buildTypes {
2632

2733
debug {
@@ -57,4 +63,10 @@ dependencies {
5763

5864
// Utility
5965
implementation deps.utility.fragmentation
66+
67+
// Test dependencies
68+
testImplementation deps.test.mockk
69+
testImplementation deps.test.junit_api
70+
testImplementation deps.test.junit_params
71+
testRuntimeOnly deps.test.junit_engine
6072
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package pl.valueadd.mvi.exception
2+
3+
import java.lang.RuntimeException
4+
5+
internal class ViewWasNotDetachedException : RuntimeException("Detach previous view first.")

mvi-valueadd/src/main/java/pl/valueadd/mvi/fragment/mvi/BaseMviPresenter.kt

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@ import io.reactivex.schedulers.Schedulers
99
import io.reactivex.subjects.BehaviorSubject
1010
import io.reactivex.subjects.PublishSubject
1111
import pl.valueadd.mvi.exception.ViewNotAttachedException
12+
import pl.valueadd.mvi.exception.ViewWasNotDetachedException
1213

1314
abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI : IBaseView.IBaseIntent, V : IBaseView<VS, VI>>(
1415
initialState: VS
1516
) : IMviPresenter<V> {
1617

18+
//region Variables
19+
1720
/**
1821
* Current view state.
1922
*/
@@ -36,8 +39,17 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
3639
* @see subscribeViewStateConsumer
3740
* @see viewStateConsumerDisposable
3841
*/
39-
protected open val viewStateSubscriptionScheduler: Scheduler =
42+
protected open val viewStateSubscriptionScheduler: Scheduler by lazy {
4043
Schedulers.io()
44+
}
45+
46+
/**
47+
* @see subscribeViewStateConsumer
48+
* @see viewStateConsumerDisposable
49+
*/
50+
protected open val viewStateObservationScheduler: Scheduler by lazy {
51+
AndroidSchedulers.mainThread()
52+
}
4153

4254
/**
4355
* Internal nullable [view][IBaseView] of presenter.
@@ -76,6 +88,10 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
7688
private val currentViewIntentsSubject =
7789
PublishSubject.create<VI>()
7890

91+
//endregion
92+
93+
//region Lifecycle methods
94+
7995
/**
8096
* If view is attached the first time, the presenter subscribes its provided intents.
8197
* Each time subscribes to view state's consumer and bind view's intents.
@@ -84,10 +100,13 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
84100
*/
85101
@CallSuper
86102
override fun attachView(view: V) {
87-
this.internalView = view
103+
if (this.internalView != null) {
104+
throw ViewWasNotDetachedException()
105+
}
88106

107+
this.internalView = view
89108
if (isViewAttachedFirstTime) {
90-
subscribeViewIntents()
109+
startObservingCurrentViewStateSubject()
91110
}
92111

93112
subscribeViewStateConsumer()
@@ -114,13 +133,15 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
114133
/**
115134
* Dispose subscribed wrapped intents and current view's intents.
116135
*
117-
* Resets presenter state.
136+
* This should be called on fragment's **destroy**.
118137
*/
119138
@CallSuper
120139
override fun destroy() {
121140
reset()
122141
}
123142

143+
//endregion
144+
124145
/**
125146
* Map view's intent actions to [provided presenter intents][BaseMviPresenter.providePresenterIntents].
126147
*
@@ -185,7 +206,7 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
185206
private fun subscribeViewStateConsumer() {
186207
viewStateConsumerDisposable = viewStateBehaviorSubject
187208
.subscribeOn(viewStateSubscriptionScheduler)
188-
.observeOn(AndroidSchedulers.mainThread())
209+
.observeOn(viewStateObservationScheduler)
189210
.subscribe(view::render)
190211
}
191212

@@ -195,7 +216,7 @@ abstract class BaseMviPresenter<VS : IBaseViewState, PS : IBasePartialState, VI
195216
*
196217
* This method should be called **once**. The best time to call it when view is attached for the first time.
197218
*/
198-
private fun subscribeViewIntents() {
219+
private fun startObservingCurrentViewStateSubject() {
199220
viewStateReducerDisposable =
200221
currentViewIntentsSubject
201222
.flatMap(::mapViewIntentToPartialState)
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package pl.valueadd.mvi.fragment.base
2+
3+
import io.mockk.every
4+
import io.mockk.mockk
5+
import io.mockk.verify
6+
import io.reactivex.Observable
7+
import io.reactivex.schedulers.Schedulers
8+
import kotlinx.android.parcel.Parcelize
9+
import org.junit.jupiter.api.BeforeEach
10+
import org.junit.jupiter.api.Test
11+
import pl.valueadd.mvi.activity.BaseActivity
12+
import pl.valueadd.mvi.fragment.mvi.BaseMviPresenter
13+
import pl.valueadd.mvi.fragment.mvi.IBasePartialState
14+
import pl.valueadd.mvi.fragment.mvi.IBaseView
15+
import pl.valueadd.mvi.fragment.mvi.IBaseViewState
16+
17+
class BaseMviFragmentTest {
18+
19+
private lateinit var mockPresenter: TestPresenter
20+
21+
private lateinit var fragment: TestMviFragment
22+
23+
@BeforeEach
24+
fun setup() {
25+
mockPresenter = mockk(relaxed = true)
26+
fragment = TestMviFragment()
27+
fragment.presenter = mockPresenter
28+
}
29+
30+
@Test
31+
fun `Should attach view to presenter on start`() {
32+
// Given
33+
34+
// When
35+
fragment.onStart()
36+
37+
// Then
38+
verify(exactly = 1) { mockPresenter.attachView(fragment) }
39+
}
40+
41+
@Test
42+
fun `Should detach view to presenter on stop`() {
43+
// Given
44+
45+
// When
46+
fragment.onStop()
47+
48+
// Then
49+
verify(exactly = 1) { mockPresenter.detachView() }
50+
}
51+
52+
@Test
53+
fun `Should call onDestroy on presenter`() {
54+
// Given
55+
val mockActivity = mockk<TestActivity>(relaxed = true) {
56+
every { supportDelegate } returns mockk(relaxed = true)
57+
}
58+
fragment.onAttach(mockActivity)
59+
60+
// When
61+
fragment.onDestroy()
62+
63+
// Then
64+
verify(exactly = 1) { mockPresenter.destroy() }
65+
}
66+
}
67+
68+
private class TestMviFragment : BaseMviFragment<TestView, TestViewState, TestViewIntent, TestPresenter>(layoutId = 0), TestView {
69+
70+
override lateinit var presenter: TestPresenter
71+
72+
override fun render(state: TestViewState) {
73+
// no-op
74+
}
75+
76+
override fun provideViewIntents(): List<Observable<TestViewIntent>> {
77+
return emptyList()
78+
}
79+
}
80+
81+
private class TestPresenter(
82+
private val mapper: TestViewIntentToPartialStateMapper,
83+
private val reducer: TestReducer
84+
) : BaseMviPresenter<TestViewState, TestPartialState, TestViewIntent, TestView>(
85+
TestViewState()
86+
) {
87+
override val viewStateSubscriptionScheduler = Schedulers.trampoline()
88+
override val viewStateObservationScheduler = Schedulers.trampoline()
89+
90+
override fun mapViewIntentToPartialState(viewIntent: TestViewIntent): Observable<out TestPartialState> {
91+
// For easier testing
92+
return mapper.mapViewIntentToPartialState(viewIntent)
93+
}
94+
95+
override fun reduce(previousState: TestViewState, action: TestPartialState): TestViewState {
96+
// For easier testing
97+
return reducer.reduce(previousState, action)
98+
}
99+
}
100+
101+
private interface TestView : IBaseView<TestViewState, TestViewIntent>
102+
103+
@Parcelize
104+
private class TestViewState(var someProperty: Int = 0) : IBaseViewState
105+
106+
private class TestPartialState(var someProperty: Int) : IBasePartialState
107+
108+
private class TestViewIntent : IBaseView.IBaseIntent
109+
110+
private class TestViewIntentToPartialStateMapper {
111+
fun mapViewIntentToPartialState(viewIntent: TestViewIntent): Observable<out TestPartialState> {
112+
throw RuntimeException("Should be mocked")
113+
}
114+
}
115+
116+
private class TestReducer {
117+
fun reduce(previousState: TestViewState, action: TestPartialState): TestViewState {
118+
throw RuntimeException("Should be mocked")
119+
}
120+
}
121+
122+
private class TestActivity : BaseActivity()

0 commit comments

Comments
 (0)