what was even the issue people had against it?
# android-architecture
u
what was even the issue people had against it?
g
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
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
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
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
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
How Michal said, Sqldelight doesn't require any mocking, you can use actual database, this is one of coolest features of it
u
@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
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
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
You write code which creates in memory db only once and use it for all your tests
u
@MiSikora okay so its just semantics, Id call that a fake driver but sure, lets call it real
m
It’s not fake and it’s not semantics. It’s an actual driver that will execute SQL statements.
u
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
You can use Dagger, but for this use case it looks as overkill, you will have dagger component for a couple dependencies
u
often you need other dependencies as well like api, so its not just as easy to type this hierarchy
g
Also you may want to provide different implementations, which not so easy with Dagger
u
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
I don't see why need retrofit there, just provide stub implementation of service
m
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
@MiSikora do you use @Modules for you own ctors or @Inject?
m
Both
Oh, for my own.
@Inject constructor()
.
u
well, if you were to create Modules for all you'd have your factories right there imo
youre duplicating DI (sub) graph
g
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
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
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
Yup. All adapter could be hidden behind one class that you would expose outside.
u
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
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
why, this is setup, no logic, this is exactly where it should be imo, wiring
m
Are you really only wiring though? There is a lot of instantiation.
u
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
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
right, but Interactor test, why should it be concerned that there is a database underneath the Repo? if it only deals with Repo?
m
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
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
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
yes -- hence Im not arguing hashmap over jdbc, none issue
im argunig of initing the db fixtures, in a Interactor test
m
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
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
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
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
What do you mean by initialize? Create, start and connect or prepare some data for a test case?
u
prepare data
m
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
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
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
but is userStore that presenter's direct dependency or transitive?
m
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
right, and the transitive initializing I feel like is crossing concens, pragmatic sure, but were talking design now, no?
m
And what is the other option? How would you prepare test data in this case?
u
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
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
well thats what im saying that it doesnt, hence som sort of test specific dsl is needed I guess
m
And I write those if initialization gets complex
For trival cases I’m too lazy
u
alright so yuo chose to be pragmatic
m
I hope so 😅
u
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
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
yes Im most lazy and wouldnt bother to write em
thank you!
👍 1