7mind / izumi

Productivity-oriented collection of lightweight fancy stuff for Scala toolchain

Home Page:https://izumi.7mind.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

[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.

commented

@lukoyanov

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.