[Question] Global start/bootstrap logic before all tests
lukoyanov opened this issue · comments
Hi, Team!
First of all, thank you all for the awesome Scala FP DI Framework. We use it a lot.
Today, I have one question regarding the best way of implementing global bootstrap logic to be run before all tests.
Currently, we do it like this
// Some base class for all tests
class BaseZioItSpec
extends Spec3[ZIO]
with ... {
override def config: TestConfig =
TestConfig(
pluginConfig = PluginConfig.const(DefaultPlugin),
moduleOverrides = new ModuleDef {
...
make[Unit].named("global-start").fromEffect(fixtureBootstrap _)
},
forcedRoots = Set(
...
DIKey.get[Unit].named("global-start")
),
memoizationRoots = Set(
...
DIKey.get[Unit].named("global-start"),
)
)
...
/** Creates test data */
def fixtureBootstrap(
xa: Transactor[Task],
someRepository: SomeRepository,
userService: UserService
): UIO[Unit] = {
for {
_ <- UIO(println("Bootstrapping data for tests"))
_ <- userService.createUser(NewUser("test-1"...))
...
} yield ()
}
}
The problem:
Although we have our fixtureBootstrap
run once before all tests, we have a problem with the userService
and its dependencies. The arguments of fixtureBootstrap
and ALL nested dependencies (there could be quite a lot of them) of the components in the arguments are created only once and shared across all tests.
This is undesirable sometimes.
We potentially can only inject Transactor[Task]
into fixtureBootstrap
only and duplicate some logic from other components like userService
to "unblock it" and make it re-creatable on every test, but there may be another way of doing it.
Please kindly advise how we can define a global bootstrap logic without pinning some portion of the instances in the DI graph.
Thank you.
We potentially can only inject Transactor[Task] into fixtureBootstrap only and duplicate some logic from other components like userService to "unblock it" and make it re-creatable on every test, but there may be another way of doing it.
This is what we do in general.
Alternatively you can make named copies of the graphs that you use in the startup task. Though that may be bothersome to do - you'll need to assign names to all the components AND to their dependencies - to prevent dependencies from being memoized:
val constructorOfUserService = AnyConstructor[UserService]
val constructorOfSomeRepository = AnyConstructor[SomeRepository]
def makeAllDependenciesNamed[A](functoid: Functoid[A], name: Identifier): Functoid[A] = {
val newProvider = functoid.get.replaceKeys {
case DIKey.TypeKey(tpe, m) =>
DIKey.IdKey(paramTpe, name.id, m)(name.idContract)
case k => k
}
Functoid(newProvider)
}
val globalModule = new ModuleDef {
make[UserService].named("global").from(makeAllDependenciesNamed(constructorOfUserService, "global"))
make[SomeRepository].named("global").from(makeAllDependenciesNamed(constructorOfSomeRepository, "global"))
... make[SomeRepositoryDep].named("global").from(...) ...
}
There could be a better solution for this in the future with private bindings
Thank you for the details and provided snippet @neko-kai!
I'll give it a try with makeAllDependenciesNamed
, and will wait for the private bindings to be available in future releases.