Right but in 5.0 you can. I'll double check that t...
# kotest
s
Right but in 5.0 you can. I'll double check that there's no changes required from a user perspective
d
I think I'm missing something... is there some kind of documentation on best practices in making extensions/listeners?
s
Not yet. Once 5.0 is out I plan to overhaul the docs on this. If you have specific questions I can answer here.
d
Say something like this:
Copy code
class KtorExtension(val module: Application.() -> Unit) : TestListener {
    lateinit var appEngine: TestApplicationEngine

    lateinit var clientEngine: HttpClientEngine


    override suspend fun beforeTest(testCase: TestCase) {
        appEngine = TestApplicationEngine(createTestEnvironment())
            .also { it.application.module() }

        clientEngine = TestHttpClientEngine.create { app = appEngine }

        appEngine.start()
    }

    override suspend fun afterTest(testCase: TestCase, result: TestResult) {
        appEngine.stop(0L, 0L)
    }
}
is this the right way to do it?
Or should I be using an extension?
s
Yep that's fine, although I would do this:
Copy code
class KtorExtension(val module: Application.() -> Unit) : BeforeTestListener, AfterTestListener {
    lateinit var appEngine: TestApplicationEngine

    lateinit var clientEngine: HttpClientEngine


    override suspend fun beforeTest(testCase: TestCase) {
        appEngine = TestApplicationEngine(createTestEnvironment())
            .also { it.application.module() }

        clientEngine = TestHttpClientEngine.create { app = appEngine }

        appEngine.start()
    }

    override suspend fun afterTest(testCase: TestCase, result: TestResult) {
        appEngine.stop(0L, 0L)
    }
}
d
Why?
s
Because TestListener is just a combination of 10 interfaces that is around for legacy reasons. It won't be removed though.
d
And so how are extensions different, and when would they apply?
s
An
extension
is anything you write that plugs into the kotest lifecycle - so before/after test/spec/project / etc. A
listener
is an older term we had that basically means "an extension that is read only". So notice in your example that you are notified of before/after test but you can't "change the engine". So that's a listener. An extension is just more powerful can do things like override test results, or skip tests entirely, etc.
d
So why are you deprecating listener(...) if it has a different purpose?
s
because inside the code base listener extends extension. A listener is an extension, it's just a term for "simple extension" that really serves no purpose being it's own interface.
TestListener
won't be renamed as it's used by everyone, but what does this do:
Copy code
interface Listener: Extension
It's irrelevant because internally, the code works on
Copy code
interface Extension
Listener is just a marker interface serving no purpose other than to fuel discussions like this on "what is listener vs extension"
d
Oh... that's a bit confusing on the user's side though... Since we're supposed to know that a listener is just another extension... after you explained it, it seems like both are useful... but you're right that having two ways to install the same thing isn't great either... I wonder if there's a way to make all this clearer.
s
This is meant to make things clearer. So instead of
Copy code
class MyTest : FunSpec() {
  override fun extensions() = listOf(foo)
  override fun listeners() = listOf(bar)
}
Just do
Copy code
class MyTest : FunSpec() {
  override fun extensions() = listOf(foo, bar)
}
everything is an extension, regardless of whether it's called an extension, a listener, or a filter, or a foo
I'll write up a page that goes over this in detail to hopefully make things clear and I'll circulate before we release 5.0
d
Yeah, but it's not to evident that
BeforeTestListener
is an extension when the concept of
Extension
is something a bit more flexible. I guess the docs could be a big help do disambiguate things though...
s
Right, the nomenclature certainly isn't the best, for legacy reasons
but I think it's a clearer message to say "listeners are just extensions anyway" than "we have two parallel hierarchies of stuff"
👍🏼 1
d
By the way, I was wondering what would be the most appropriate way to integrate Dagger2 into an extension to to dependency injection... also having a way to use the component's val's and functions without aliasing them
val dep1 get() = component.dep1()
would be nice...
I'd have to hook on to the test class creation somehow and maybe even make it a delegate of the component...
s
How does dagger integrate normally, outside of tests ?
is there a method you call like "setup my class" ?
d
If I do:
Copy code
class SomeSpec @Inject constructor(val dep1: Dep1) { ... }

// component interface
interface TestComponent {
   val someSpec: SomeSpec
}

// Will generate DaggerTestComponent, and then to build the graph:
val component = DaggerTestComponent.builder().build()

// And to retreive the injected spec:
component.someSpec
s
how does
someSpec
get inside the component ?
d
Dagger2 generates
DaggerTestComponent
that derives from
TestComponent
with all the object initialization code
s
Through a compiler plugin ?
d
Through kapt
s
You could write something like:
Copy code
class DaggerExtension : PostInstantiationExtension {
  override suspend fun instantiated(spec: Spec): Spec {
    // get component somehow ? by casting ?
    return component.someSpec
  }
}
Or look at
Copy code
ConstructorExtension
which allows you to control how the spec is created from the class
d
Thanks! I'll take a look at those.
s
if you get stuck I can help
d
Ok, a bit funny... I have say:
Copy code
interface TestComponent {
  fun inject(spec: SomeSpec)
}

val component = DaggerTestComponent.builder().build()

class SomeSpec : DescribeSpec() {
  @Inject val dep1 : Dep1
}
I have to get the
PostInstantiationExtension
to run
component.inject(this@SomeSpec)
somehow... so that dep1 gets injected...
But how is the extension supposed to guess that TestComponent has an inject function that takes the SomeSpec type...
The start was:
Copy code
class  DaggerExtension<C : Any, S : KotestDbSpec>(val component: C, val injector: (S) -> Unit) : PostInstantiationExtension {
    override fun process(spec: Spec): Spec = spec.also { component/*...?*/ }
}
s
Well, you could look for an annotation that tells it it's a dagger test, or you could do some reflection magic if something is possible there, or you could just say this extension should only be added to dagger specs and you cast and catch
d
Copy code
class  DaggerExtension<C : Any, S : KotestDbSpec>(val component: C, val injector: C.(S) -> Unit) : PostInstantiationExtension {
    override fun process(spec: Spec): Spec = spec.also { component.injector(spec as S) }
}
?
s
I don't know anything about dagger. How do you know something is dagger normally?
Does it generate a new class, or new functions, what does kapt add
d
A DaggerTestComponent gets generated with something like:
Copy code
@Override
  public void inject(GetCustomerSpec getCustomerSpec) {
    injectGetCustomerSpec(getCustomerSpec);
  }

  private GetCustomerSpec injectGetCustomerSpec(GetCustomerSpec instance) {
    GetCustomerSpec_MembersInjector.injectCustomerRepo(instance, customerTestRepoProvider.get());
    return instance;
  }
s
Is it called
DaggerTestComponent
and is there one per spec ?
or one giant one
d
The generated class is Dagger{name of the component interface}
s
So then inside your extension, could you do
Class.forName(Dagger + kclass.name)
to get the class reference
Then you can use reflection to create the instance, call inject
that could easily be a ConstructorExtension
d
And for each spec that needs injection, I would have to add an
fun inject(spec: SomeSpec)
, I don't need to create the spec, I just need to run
DaggerTestComponent.builder().build().inject(this)
before each test or spec.
I just need to find the appropriate inject function...
I suppose
Spec
doesn't contain it's type or something...
s
Spec::class is the spec's class
Right, so this is a start
Copy code
class DaggerExtension : PostInstantiationExtension {
   override suspend fun instantiated(spec: Spec): Spec {
      val c = Class.forName("Dagger" + spec::class.name) // this would be DaggerMyTest
      val m = c.declaredMethods.find { it.name == "builder" ) // get builder() method
      val builder = m.invoke(null) as DaggerBuilder
      DaggerBuilder.build().inject(spec)
      return spec
   }
}
I didn't test any of that but it's probably in the right direction
d
Class.forName("Dagger" + spec::class.name)
isn't the spec, it should be
Class.forName("Dagger" + componentInterface::class.name)
... say
TestComponent
would have
DaggerTestComponent
implementation generated that contains a function
inject(spec: SomeSpec)
which is the function that I need to run...
s
Well it's a start anyway
doing a prod release now, back later