https://kotlinlang.org logo
#android-architecture
Title
# android-architecture
u

ursus

08/18/2020, 8:24 PM
Testing people who use fakes. Do you fake all the layers only the "edge" ones like db or api? Since I want to test a viewmodel now, and then have to recreate the whole stack just to swap the fake api at the end, and its so stupid, so much code Why not just mockito stub out the direct view model dependencies?
👍 1
j

John Leeroy

08/18/2020, 8:28 PM
What’s the scope of your tests? Unit, Integration, end to end, etc
u

ursus

08/18/2020, 8:33 PM
I dont really know what the delineation, what I want to test is this
Copy code
viewModel.someAction()
viewModel.stateObservable.test().assertValue(...)
j

John Leeroy

08/18/2020, 8:37 PM
Sounds like unit tests where the scope is within the method of
someAction
and the acceptance criteria is
viewModel.stateObservable.test().assertValue(...)
. I would recommend mocking any dependencies that
someAction
will delegate responsibility to and pretend it returns the expected result. This approach only have you mock adjacent classes.
u

ursus

08/18/2020, 8:38 PM
well, yes, but dont fakes people tell you not to do that and use real implementations, and only fake out the edges?
it feels like I need dagger components in tests
j

John Leeroy

08/18/2020, 8:45 PM
If you want to test ViewModel + Data Source, you’re probably more along the lines of integration testing (and possibly instrumented test which require a device). You can create fakes at the edges and have those flow through to your view model. It is a lot more testing code to implement. It takes a lot more effort than unit testing. I would definitely evaluate what you’re testing and identify which level of testing helps you achieve your testing goals.
u

ursus

08/18/2020, 8:51 PM
well, you tell me, lets say code looks like this
Copy code
class LoginViewModel(initialState: State, authManager: AuthManager, ...) {
	init {
		authManager.loginState
			.subscribe {
				when {
					Loading -> setState { copy(logginIn = true) }
					Success -> setState { copy(logginIn = false) }
				}
			}
	}
	fun loginClicked() {
		authManager.login(..)
	}

	data class State(loggingIn: Boolean = false)
}


@Test fun test() {
	val viewModel = createLoginViewModelWithAllTransitiveDependenciesLikeACrazyPerson()
	val stateObserver = viewModel.state.test()

	viewModel.loginClicked()
	stateObserver.assertValues(State(logginIn = true), State(logginIn = false)
}
and In order to create a real AuthManager only to swap the very top retrofit AuthApi, I need like 20 objects..duplicating all of my dagger code almost
all for this
Copy code
class FakeAuthApi {
	fun login(): Single<Tokens> {
		return Single.just(Tokens("fake-access-token", "fake-refresh-token"))
	}
}
j

John Leeroy

08/18/2020, 8:54 PM
I highly recommend reading the documentation on testing. You don’t have to do all that work if you understand your own goals for testing and how that aligns with standard practice (check out the pyramid). If you want a short answer: write unit tests for your view model and mock all the dependencies.
u

ursus

08/18/2020, 8:54 PM
whereas yes, mocking out `ąuthManager.loginState`˛would sound sane
I read that some time ago, doesnt really talk about this mockito bad, fakes good movement
Anyways, lets say its a medium test
Vertical slices of your app, testing interactions on a particular screen. Such a test verifies the interactions throughout the layers of your app's stack.
same issue, do I use real impls and only fake out the edges?
j

John Leeroy

08/18/2020, 9:01 PM
I would say use real implementations for all the components you want to test and mock/stub/fake everything else (especially data sources).
u

ursus

08/18/2020, 9:01 PM
what do you mean use real implementations for components I want to test, like transitively?
okay lets say I agree, that I want to test the whole login, so .. view model clicks, observe its progressbar, and also observe tokens being saved to dao
j

John Leeroy

08/18/2020, 9:03 PM
Medium or integration tests will include several class/components. To properly test their interactions, you’ll need to use the real implementations. If your integration test doesn’t care about using a real API/Database/etc, then you can mock that aspect of it.
u

ursus

08/18/2020, 9:04 PM
yes, however, the setup is insane, I need like 50 lines of constructors
j

John Leeroy

08/18/2020, 9:04 PM
If you want to test the experience as a user would, the category of testing is considered Acceptance Testing or UI Tests. Look into Espresso or Appium as examples for that.
u

ursus

08/18/2020, 9:05 PM
doesnt matter, same issue, you only go one layer further with espresso
so, dagger it is I guess, I remember people using test components in espresso
so, do you use mockito yourself, or do you interface-everything
j

John Leeroy

08/18/2020, 9:15 PM
I want to re-iterate that you should look into standard testing practices for Android. Here's a pretty comprehensive article. As far as coding goes, I commonly use an interface to define boundaries and separate layers. It's generally good to use a lot of interfaces because it also makes testing easier thanks to the ability to mock them easily. The two work hand-in-hand.
u

ursus

08/18/2020, 9:18 PM
my issue is that UI testing is very broken even with espresso, I'm waiting for compose for that and, "integration tests" means 10 different things to 10 different people, only common denominator is just tests "integration" whatever that is
☝️ 2
💯 1
how does a test with mocking your interface manually looks like? Do you have just anonymous subclasses containing all the methods, in all tests, even when only testing lets say login in this test case?
Copy code
@Test test1 {
	val authManager = object: AuthManager {
		override val logginIn: Observable<Boolean>
			get() = Observable.just(false) <---

	    override fun loginUser(code: String) {
	    }
	    override fun refreshUser(refreshToken: String) {
	    }
	    override fun logoutUser() {
	    }
	}
	....
	
}

@Test test2 {
	val authManager = object: AuthManager {
		override val logginIn: Observable<Boolean>
			get() = Observable.just(true) <---

	    override fun loginUser(code: String) {
	    }
	    override fun refreshUser(refreshToken: String) {
	    }
	    override fun logoutUser() {
	    }
	}
	....
	
}
g

gildor

08/19/2020, 2:17 AM
For your LoginViewModel test I would just use Fake/Stub implementation of Authmanager, looks completely reasonable
all the methods, in all tests
Looks like work for test fixtures, you create one implementation of this AuthManager which used for tests (so you could easily return any data from it and reuse it in all tests
for unit tests you don’t need “all graph” of course, only dependenciec which easy to use for unit tests
you can use mocks, but honestly in case of AuthManager I would rather use fake
and it still doesn’t look as integration test, rather as unit, but as I see your examples, all of them look as unit
u

ursus

08/19/2020, 3:11 AM
So youd fake out even AuthManager which by it self is not a edge dependency?
g

gildor

08/19/2020, 3:12 AM
If it unit test, of course
if you want to test AuthManager too in the same test, it looks more like integration test indeed, which can be done as test on JVM (as you pointed out requires a lot of dependencies), or as an instrumentation test without ui, so you could mock just some dependencies But when I see your example, it clearly looks for me as unit test
💯 1
I use real dependencies where it’s possible, to make it closer to actual code, but there is always balance between simplicity and what you actually want to test, in this case looks that you want to test LoginViewModel
💯 1
Copy code
class FakeAuthManager(isLoggedIn: Boolean, initialUser: User? = ...) : AuthManager {
    private val logginInSubject = BehaviorSubject.createDefault(isLoggedIn)
    override val logginIn = logginInSubject.hide()
    
    override fun loginUser(code: String) {
        // do some valiudation if you want
        logginInSubject.onNext(true)
    }
    override fun refreshUser(refreshToken: String) {
        // you can add api to this fake to specify new user
    }
    override fun logoutUser() {
        logginInSubject.onNext(false)
    }
}
something like this
so you ahve class that behaves like real one but easy to control from tests with any behaviour what you want
u

ursus

08/19/2020, 6:07 PM
hmm interesting, thanks!
btw if I were to go with a integration test, i.e. as much real stuff as I can, as to test the whole login stack. What would you assert -- or rather, is it okay to assert multiple things in one test? Basically what I'd like to check is the viewModel.state.loggingIn boolean (implies progressbar), and also saving of auth tokens and User object to database
Copy code
viewModel.state.test().assertValues(true, false)
assertEquals tokensDb.tokens, Tokens("abc123", "cba321")
assertEquals userDb.users, User(..)

is this cool in 1 test case? or is this 3 cases
j

John Leeroy

08/19/2020, 11:45 PM
If you're implementing a login-related integration test, it would make sense to have multiple asserts that can cover your view model and the database.
1
3 Views