badoo / MVICore

MVI framework with events, time-travel, and more
https://badoo.github.io/MVICore/
Other
1.27k stars 90 forks source link

A good place for UI-related tests #125

Closed yozh1que closed 4 years ago

yozh1que commented 4 years ago

Could you provide a view on how to implement tests that cover interactions with UI. In MVVM it was solely done inside ViewModel but with MVI I'm not sure where it belongs.

Say, inside a fragment we need to combine values from several edittexts and send a wish based on some criteria:

Observable.combineLatest(
    RxTextView.textChanges(newPinEditText),
    RxTextView.textChanges(pinRepeatEditText),
    BiFunction<CharSequence, CharSequence, Boolean>() { v1, v2 -> v1.length == v2.length && v1.length == limit }
)
...
.map{ some wish }

Is there an mvi way to test the logic before the map?

I know there's a concept of actor middleware that deals with async requests and other backend logic - do you think it should also cover ui-related checks? Wouldn't it bloat State?

Thanks!

himattm commented 4 years ago

Why do you need an MVI way to test the logic before the map? You should just be able to test the logic to see if you are getting the output you are expecting. Your Wish shouldn't be doing any logic so as long as the logic to give it what it wants works you can safely assume you have what you need.

However, if you want you can simulate text changes with Observable.just and ensure that the logic works as intended (but again, it seems that the logic is what you are truly trying to test here).

Use a TestSubscriber to subscribe to the Observable and check that your SomeWish is equal to the one that is created.

For instance:

val expectedWish = SomeWish(result = true)
val testSubscriber = TestSubscriber<SomeWish>()

Observable.combineLatest(
    Observable.just("1", "2", "3"),
    Observable.just("1", "2", "3"),
    BiFunction<CharSequence, CharSequence, Boolean>() { v1, v2 -> 
        v1.length == v2.length && v1.length == limit
    }
)
// ... do logic
.map { result -> SomeWish(result) }
.subscribe(testSubscriber)

testSubscriber.assertValues(expectedWish);
ShikaSD commented 4 years ago

Hey, usually we connect those things through the binder e.g.

val from = Observable.combineLatest(
    Observable.just("1", "2", "3"),
    Observable.just("1", "2", "3"),
    BiFunction<CharSequence, CharSequence, Boolean>() { v1, v2 -> 
        v1.length == v2.length && v1.length == limit
    }
)

binder.bind(from to feature using SomeLogic)

SomeLogic in the example above can be either:

These functions can be tested separately, e.g. you can provide Observable<A> to connector and check that Observable<B> is emitting something matching your expectations.

Note that you can do it without binder as well, just extracting logic between combineLatest and map to a separate function/class.

yozh1que commented 4 years ago

Thanks for your suggestions, guys. I ended up relaying events to the actor and handling ui logic there. I also had to expand state objects to include ui-related fields (like "typed password at full length, error should be displayed") which, I now think, is not a bad thing - it makes view "dumb", isolates logic from the framework and empowers tdd.