appmattus / kotlinfixture

Fixtures for Kotlin providing generated values for unit testing
Apache License 2.0
263 stars 15 forks source link

Benefits to creating a top-level generator? #98

Open romrell4 opened 1 year ago

romrell4 commented 1 year ago

Hey @mattmook - love the library. Thanks for all the work you put into it. I'm proposing that we use it at Walmart to support our tests/sample apps. There's a pretty rigorous approval process before Walmart allows us to pull in external dependencies, especially those created and maintained by an individual. So before I propose the library to the powers-that-be, I'd love to make sure I completely understand how to best use the library.

So in our code, we often create "factory" functions for domain classes that provide base-level defaults, but support injecting whatever data is necessary. It might look something like this: in src/main/java...

data class Foo(
  val bar1: String,
  val bar2: Int,
  val bar3: List<SomeObj>,
  val bar4: String?,
)

in src/test/java/Fixtures.kt

fun createFoo(bar1: String = "", bar2: Int = 0, bar3: List<SomeObj> = listOf(), bar4: String? = null) = Foo(bar1, bar2, bar3, bar4)

and would be used in a test like

@Test
fun `test that the result is true when when bar2 is negative`() {
  val result = viewModel.doSomething(createFoo(bar2 = -1))
  assertThat(result).isTrue()
}

Obviously, the downside of this is that anytime a new variable gets added to Foo, the factory function has to be updated to support the new default value as well. We also work in a massively-multi-module-monolith, so it means that each module that depends on that Foo class would have to create it's own generator (we're looking at options for sharing test-time code, but that's aside from my current investigation) - in which case many generators would have to be updated to support the one new variable.

Alright, so there's some background. That all being said, we'd love to replace our factory function with your library. However, it seems you have an extra step, in that you create a top-level "Fixture", and then make multiple invocations on that generator. For instance, from your docs:

val fixture = kotlinFixture()

// Generate a list of strings
val aListOfStrings = fixture<List<String>>()

// Nulls are supported
val sometimesNull = fixture<Int?>()

// Create instances of classes
// Optional parameters will be randomly used or overridden
data class ADataClass(val value: String = "default")
val aClass = fixture<ADataClass>()

Are there memory or other reasons why you reuse your generator between invocations? Or would a helper function like:

inline fun <reified T> fixture() = kotlinFixture().invoke<T>()

work just as well? I ask because such a function would allow us to one-to-one replace our current fixture functions with your library. For instance, in the test above:

@Test
fun `test that the result is true when when bar2 is negative`() {
  val result = viewModel.doSomething(fixture<Foo>().copy(bar2 = -1))
  assertThat(result).isTrue()
}

I'd love to hear your thoughts. If such a function could be helpful to general public, I'm happy to submit a PR to contribute to the library. If you don't think so, I could keep that helper function just in Walmart's codebase.

Thanks in advance! And sorry if this isn't really an "issue" - just looked like the best place to start the discussion :)

romrell4 commented 1 year ago

From my investigation, it looks like the Fixture class that is returned from kotlinFixture is a very lightweight class that doesn't retain any state outside of it's configuration. So in the case that a user doesn't require a top-level configuration, would it be helpful to support a:

inline fun <reified T> fixture() = kotlinFixture().invoke<T>()

function that creates the mock in a one-liner, rather than making them go through the middle hoop?

romrell4 commented 1 year ago

In case you do like the idea, here's a quick PR for it