u

    ursus

    2 years ago
    what was even the issue people had against it?
    gildor

    gildor

    2 years ago
    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

    2 years ago
    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))
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    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?
    MiSikora

    MiSikora

    2 years ago
    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.
    gildor

    gildor

    2 years ago
    How Michal said, Sqldelight doesn't require any mocking, you can use actual database, this is one of coolest features of it
    u

    ursus

    2 years ago
    @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
    gildor

    gildor

    2 years ago
    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
    MiSikora

    MiSikora

    2 years ago
    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.
    gildor

    gildor

    2 years ago
    You write code which creates in memory db only once and use it for all your tests
    u

    ursus

    2 years ago
    @MiSikora okay so its just semantics, Id call that a fake driver but sure, lets call it real
    MiSikora

    MiSikora

    2 years ago
    It’s not fake and it’s not semantics. It’s an actual driver that will execute SQL statements.
    u

    ursus

    2 years ago
    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
    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?
    gildor

    gildor

    2 years ago
    You can use Dagger, but for this use case it looks as overkill, you will have dagger component for a couple dependencies
    u

    ursus

    2 years ago
    often you need other dependencies as well like api, so its not just as easy to type this hierarchy
    gildor

    gildor

    2 years ago
    Also you may want to provide different implementations, which not so easy with Dagger
    u

    ursus

    2 years ago
    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
    gildor

    gildor

    2 years ago
    I don't see why need retrofit there, just provide stub implementation of service
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    @MiSikora do you use @Modules for you own ctors or @Inject?
    MiSikora

    MiSikora

    2 years ago
    Both
    Oh, for my own.
    @Inject constructor()
    .
    u

    ursus

    2 years ago
    well, if you were to create Modules for all you'd have your factories right there imo
    youre duplicating DI (sub) graph
    gildor

    gildor

    2 years ago
    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
    MiSikora

    MiSikora

    2 years ago
    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.
    In my opinion, the only reason for Dagger in tests is when there is field injection and you have instrumentation tests.
    u

    ursus

    2 years ago
    okay here is a real example from my app
    is this what you are saying?
    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 `
    val driver = JdbcSqliteDriver(<http://JdbcSqliteDriver.IN|JdbcSqliteDriver.IN>_MEMORY)
    MiSikora

    MiSikora

    2 years ago
    Yup. All adapter could be hidden behind one class that you would expose outside.
    u

    ursus

    2 years ago
    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
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    why, this is setup, no logic, this is exactly where it should be imo, wiring
    MiSikora

    MiSikora

    2 years ago
    Are you really only wiring though? There is a lot of instantiation.
    u

    ursus

    2 years ago
    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
    @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..
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    right, but Interactor test, why should it be concerned that there is a database underneath the Repo? if it only deals with Repo?
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    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
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    yes -- hence Im not arguing hashmap over jdbc, none issue
    im argunig of initing the db fixtures, in a Interactor test
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    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?
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    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?
    MiSikora

    MiSikora

    2 years ago
    What do you mean by initialize? Create, start and connect or prepare some data for a test case?
    u

    ursus

    2 years ago
    prepare data
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    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
    MiSikora

    MiSikora

    2 years ago
    rather above the mentioned line. something like that.
    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
    u

    ursus

    2 years ago
    but is userStore that presenter's direct dependency or transitive?
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    right, and the transitive initializing I feel like is crossing concens, pragmatic sure, but were talking design now, no?
    MiSikora

    MiSikora

    2 years ago
    And what is the other option? How would you prepare test data in this case?
    u

    ursus

    2 years ago
    well, have some sort of initializer on a direct dependency
    but for that to be transparent it would need some sort of dsl ugh
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    well thats what im saying that it doesnt, hence som sort of test specific dsl is needed I guess
    MiSikora

    MiSikora

    2 years ago
    And I write those if initialization gets complex
    For trival cases I’m too lazy
    u

    ursus

    2 years ago
    alright so yuo chose to be pragmatic
    MiSikora

    MiSikora

    2 years ago
    I hope so 😅
    u

    ursus

    2 years ago
    I dont think there is any other option without a dsl to some intermediary format, which is then internally mapped to the db api
    MiSikora

    MiSikora

    2 years ago
    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

    2 years ago
    yes Im most lazy and wouldnt bother to write em
    thank you!