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

ursus

05/24/2020, 12:17 PM
what was even the issue people had against it?
g

gildor

05/24/2020, 2:04 PM
In unit tests you should test particular class, 8f it requires init of whole infrastructure, it's not a unit test anymore, but integration test
u

ursus

05/24/2020, 2:05 PM
That might be textbook definition, but take a real case of testing a Repo call
you need Api and Dao. Say you use SqlDelight and you can swap out android driver for jdbc one. So ctors looks like this
Dao(Database(Driver))
tldr;
Repo(Dao(Database(_JdbcDriver_))))
so in order to swap the driver I'd need to reimplement the whole hierarchy, or use composition and override where needed bla bla too much work to create
FakeRepo(FakeDao(FakeDatabase(jdbcDriver))
m

MiSikora

05/24/2020, 2:35 PM
Why would you need any fakes or reimplement anything in this scenario? You do not need to fake anything. You provide JDBC driver and use it in tests with an in-memory database. Any other layers stay the same since you do not need to fake any interactions.
u

ursus

05/24/2020, 2:44 PM
well sure, by fake I mean the jdbc in this case..anyways, how do you swap the android driver for jdbc one in such higher level test?
m

MiSikora

05/24/2020, 2:48 PM
I write test factories with default arguments that provide fakes where needed. This way dependencies can be always changed or have their behaviour prerecorded since fakes are avaialble from the factory and I can interact with it.
g

gildor

05/24/2020, 2:48 PM
How Michal said, Sqldelight doesn't require any mocking, you can use actual database, this is one of coolest features of it
u

ursus

05/24/2020, 2:50 PM
@MiSikora by fakes you mean the value types? or behavior like Repository etc
@gildor yes exactly, but if youre testing FooInterfactor which needs FooRepository which needs FooDao which needs FooQueries which need Database which needs JdbcDriver --- so only swapping out the jdbc driver object at the end
g

gildor

05/24/2020, 2:52 PM
What is problem to swap driver? Driver requires only when you create db, create db in tests, wrap to dao if it's necessary, pass to repository
m

MiSikora

05/24/2020, 2:52 PM
By fakes I mean stuff like
Clock
so I can control time, any dependencies that are provided by framework (like location provider) and sometimes web services if I do not care about HTTP layer. I definitely do not fake repositories.
g

gildor

05/24/2020, 2:52 PM
You write code which creates in memory db only once and use it for all your tests
u

ursus

05/24/2020, 2:53 PM
@MiSikora okay so its just semantics, Id call that a fake driver but sure, lets call it real
m

MiSikora

05/24/2020, 2:53 PM
It’s not fake and it’s not semantics. It’s an actual driver that will execute SQL statements.
u

ursus

05/24/2020, 2:54 PM
in this case I dont think it matters if there is a hashmap or jdbc with in memory flag underneath but okay, Im not arguing that at all
-- what Im arguing is to create this
Copy code
testing FooInterfactor which needs FooRepository which needs FooDao which needs FooQueries which need Database which needs JdbcDriver
you are recreating dagger module provider functions, so why not just..use them?
g

gildor

05/24/2020, 2:56 PM
You can use Dagger, but for this use case it looks as overkill, you will have dagger component for a couple dependencies
u

ursus

05/24/2020, 2:56 PM
often you need other dependencies as well like api, so its not just as easy to type this hierarchy
g

gildor

05/24/2020, 2:57 PM
Also you may want to provide different implementations, which not so easy with Dagger
u

ursus

05/24/2020, 2:57 PM
and to create Api you need Retrofit, which needs Moshi which needs all your custom JsonAdapters etc
well, its not because some "said so" that its wrong to override module methods
g

gildor

05/24/2020, 2:57 PM
I don't see why need retrofit there, just provide stub implementation of service
m

MiSikora

05/24/2020, 2:57 PM
But there is a difference. Hash map will not execute and test your SQL queries. It is also ok to use a fake in such scenarios if you do not particularly care about SQL in this test. As for Dagger I don’t see why would you do it. It’s harder to write glue logic for tests, especially in multi module projects since you would need to write test Dagger modules where simple factories that can be easily shared suffice.
u

ursus

05/24/2020, 2:58 PM
@MiSikora do you use @Modules for you own ctors or @Inject?
m

MiSikora

05/24/2020, 2:58 PM
Both
Oh, for my own.
@Inject constructor()
.
u

ursus

05/24/2020, 2:59 PM
well, if you were to create Modules for all you'd have your factories right there imo
youre duplicating DI (sub) graph
g

gildor

05/24/2020, 3:01 PM
Yes, but it small and manually written easier to modify
Again, up to you. But we have many such tests and dont see any need in dagger, it would be a lot more tedious to implement all those small dagger components
m

MiSikora

05/24/2020, 3:02 PM
I would have just an interface / abstract class but I would be missing the whole glue that Dagger generates (unless I write also test component and modules). I see this bringing a lot of headache in the future.
1
In my opinion, the only reason for Dagger in tests is when there is field injection and you have instrumentation tests.
u

ursus

05/24/2020, 3:06 PM
okay here is a real example from my app
is this what you are saying?
Copy code
fun createTestFooRepository() {
	val moshi = Moshi.Builder()
            .add(AppointmentRequestState::class.java, AppointmentRequestStateJsonAdapter())
            .add(Uri::class.java, UriJsonAdapter())
            .build()

	val channelAdapter = Channel.Adapter(
        permissionsAdapter = PermissionsColumnAdapter(moshi)
    )
    val fileStateColumnAdapter = FileStateColumnAdapter()
    val stringListColumnAdapter = StringListColumnAdapter(moshi)

    val messageAdapter = Message.Adapter(
        typeAdapter = MessageTypeColumnAdapter(),
        stateAdapter = MessageStateColumnAdapter(),
        reactionsAdapter = DbReactionsColumnAdapter(moshi),
        f_stateAdapter = fileStateColumnAdapter,
        f_uploadSourceInfoAdapter = UploadSourceInfoColumnAdapter(moshi),
        a_attendeesAdapter = AttendeesColumnAdapter(moshi),
        a_myStatusAdapter = AppointmentRequestStateColumnAdapter(),
        e_recipientUsernamesAdapter = stringListColumnAdapter,
        e_ccUsernamesAdapter = stringListColumnAdapter
    )
    val emailAttachmentAdpater = EmailAttachment.Adapter(
        fileStateAdapter = fileStateColumnAdapter
    )

    val accountAdapter = Account.Adapter(
        themeAdapter = ThemeColumnAdapter(),
        backgroundTypeAdapter = BackgroundTypeColumnAdapter()
    )

	val driver = JdbcSqliteDriver(<http://JdbcSqliteDriver.IN|JdbcSqliteDriver.IN>_MEMORY)
    val database = Database(driver, accountAdapter, channelAdapter, emailAttachmentAdpater, messageAdapter)

   	return FooRepository(
   		FooDao(FooQueries(database), BarQueries(database),
   	)
}
this is bananas just to swap the `
Copy code
val driver = JdbcSqliteDriver(<http://JdbcSqliteDriver.IN|JdbcSqliteDriver.IN>_MEMORY)
m

MiSikora

05/24/2020, 3:07 PM
Yup. All adapter could be hidden behind one class that you would expose outside.
u

ursus

05/24/2020, 3:07 PM
yes, and its called a module function lol
NetModule.provideMoshi(), DataModule.provideDatabase(moshi, driver)
maybe technically its not really Dagger in this case, just so happens to be annotated with dagger stuff
m

MiSikora

05/24/2020, 3:12 PM
Yeah, for me it kind of looks like there is too much work in Dagger modules. Generally I try to avoid treating Dagger modules as any logical unit and use them only for provisioning of what is already available.
u

ursus

05/24/2020, 3:12 PM
why, this is setup, no logic, this is exactly where it should be imo, wiring
m

MiSikora

05/24/2020, 3:13 PM
Are you really only wiring though? There is a lot of instantiation.
u

ursus

05/24/2020, 3:13 PM
so? theres no logic at all
Id actually not want this setup in my domain code, but anyways
if you were to use your or mine approach, you need some sort of
init
function to jumpstart the db in a state you want, right? which means you need such method at every level, or in all those functions somehow
Copy code
@Test fun testWhatever() {
	val repo = createTestFooRepository() { db ->
		// init db
	}
}
like this?
I mean.. I really like the fakes approach over mockito, yes it was handcuffs, but this approach is crossing layers, and I've somehow been taught that unit tests should only test the one layer or idk how else to articulate, you dont need json parsing to test api, etc
I think for Interactor test which uses Repository, you should init and assert on the Repo, not the db..
m

MiSikora

05/24/2020, 3:19 PM
Not sure what are you asking about. Do you mean if I prepare data for test methods or do I create and open database? As for unit tests I dont know. For me dividing and trying to categorise tests between unit tests, integration tests and so on is pointless. I’m only interested in testing behaviours since it is only thing that matters. It can be for simple class, methods or event whole presenters or applications. It really doesn’t matter. The only thing that is important if given input produced expected output with a code that my app will actually run.
u

ursus

05/24/2020, 3:20 PM
right, but Interactor test, why should it be concerned that there is a database underneath the Repo? if it only deals with Repo?
m

MiSikora

05/24/2020, 3:25 PM
I prefer to use as much real code as possible and tests as big behaviours as possible. The problem with any mocks, stubs or even fakes is that if you remove your actual implementation the tests still pass but the application does not run. If you use fakes it’s important to test them against real implementation for two reasons. First that I mentioned is that it might happen that you delete the real code and tests still pass because it uses a fakes. The other is that there might be slight behaviour changes between real implementation and fake one and you cannot be 100% sure that the code will behave in the same way with a fake and with the real implementation (unless you actually check it with
Burst
or any other parametric test tool). For these reasons I prefer to use real code used in the app and fake the stuff only when I need to control it (like time, or framework elements).
The situation with mocks is even worse but that’s whole another argument.
u

ursus

05/24/2020, 3:28 PM
yes I had the same worry at the beginning, but its easy to test fakes
so yes obviously jdbc is way better than hashmap backed impl
im talking about the scoping now
m

MiSikora

05/24/2020, 3:29 PM
When you get into more complex SQL queries it gets sometimes trickier. And when you consider table restrictions, triggers and so on
For simple CRUD stuff, sure. I would be very confident that HashMap will behave more or less the same
u

ursus

05/24/2020, 3:30 PM
yes -- hence Im not arguing hashmap over jdbc, none issue
im argunig of initing the db fixtures, in a Interactor test
m

MiSikora

05/24/2020, 3:32 PM
Well, as I mentioned. If you are not interested in SQL in such a test, then go for it. I personally, would test it with DB because that is what would actually happen in the application. It is really a matter of confidence - I have very little. 🙂
u

ursus

05/24/2020, 3:34 PM
I dont think you get me. Interactor deal with Repository. Such test should have initializer to initialize state of Repo, Then do some interfactor work you wanna test, and then assert on Repo state, correct?
m

MiSikora

05/24/2020, 3:36 PM
Depends. I would rather try to design interactor in such a way that I can test most of the stuff through its output. I very rarely test interactors and repositories though. I tend to do as much testing through presentation layer as possible. If there are some side effects that are not observed in a presentation output I do check database state or whatever happened.
u

ursus

05/24/2020, 3:37 PM
okay, that makes my case even more, if you test your presenter, how do you initialize your database state?
i.e. is there a db reference in a FooPresenter test?
m

MiSikora

05/24/2020, 3:38 PM
What do you mean by initialize? Create, start and connect or prepare some data for a test case?
u

ursus

05/24/2020, 3:39 PM
prepare data
m

MiSikora

05/24/2020, 3:41 PM
Since fake factory has reference to the DB that is hidden somewhere behind the repository I use methods like
insertContact(User(1))
where
User(id: Long)
is a factory method with prepopulated data. If behaviours require more semantics they can be hidden behind different test methods.
u

ursus

05/24/2020, 3:42 PM
so inside my
createTestFooRepo()
from above? What if you want to tweak the data per test, I'd expect you to expose some lambda with db as param, unless you create some sort of dsl or something
m

MiSikora

05/24/2020, 3:47 PM
rather above the mentioned line. something like that.
Copy code
val userStore = factory.userStore

@Test fun foo() {
  userStore.insertUser(1)
  userStore.insertUser(2)

  val presenter = factory.create()

  presenter.test {
    expectItem() shouldBe intialModelWithBothUsers

    sendEvent(RemoveContact(1))
    expectItem() shouldBe modelWithoutUser1
  }
}
If data preparation gets complex I do write DSLs and so on. The benefit is they can be reused between different modules
1
u

ursus

05/24/2020, 3:49 PM
but is userStore that presenter's direct dependency or transitive?
m

MiSikora

05/24/2020, 3:53 PM
Doesn’t matter. I tend to interact first via presenter and then via direct dependencies. But if direct dependency does not have the API for what I need (because let’s say in this scenario users are added in a completely different flow and presenter should not have any ability to insert users) I operate on transitive ones.
u

ursus

05/24/2020, 3:53 PM
right, and the transitive initializing I feel like is crossing concens, pragmatic sure, but were talking design now, no?
m

MiSikora

05/24/2020, 3:54 PM
And what is the other option? How would you prepare test data in this case?
u

ursus

05/24/2020, 3:55 PM
well, have some sort of initializer on a direct dependency
but for that to be transparent it would need some sort of dsl ugh
m

MiSikora

05/24/2020, 3:56 PM
Well, sure. If public API of direct dependency allows for it then I do it this way.
Better yet if presenter allows for it by sending events like
AddContact(1)
u

ursus

05/24/2020, 3:56 PM
well thats what im saying that it doesnt, hence som sort of test specific dsl is needed I guess
m

MiSikora

05/24/2020, 3:57 PM
And I write those if initialization gets complex
For trival cases I’m too lazy
u

ursus

05/24/2020, 3:59 PM
alright so yuo chose to be pragmatic
m

MiSikora

05/24/2020, 3:59 PM
I hope so 😅
u

ursus

05/24/2020, 4:00 PM
I dont think there is any other option without a dsl to some intermediary format, which is then internally mapped to the db api
m

MiSikora

05/24/2020, 4:01 PM
Yup, I think so as well. DSLs are nice but they are time consuming to write and if it would used only in one test suite it’s an overkill in my opinion. And it’s not that hard to refactor it to a DSL once we see some patterns from other test suites.
u

ursus

05/24/2020, 4:03 PM
yes Im most lazy and wouldnt bother to write em
thank you!
👍 1
7 Views