Creating an injectable util-app App

Lifecycle

If you haven’t already, take a look at the util-app App lifecycle documentation.

Getting Started

To create an injectable util-app App, first depend on the inject-app library. Then use the inject framework to create an injectable App. Finatra provides an injectable version of the util-app App trait: c.t.inject.app.App.

Extending the c.t.inject.app.App trait creates an injectable util-app App.

This allows for the use of dependency injection in a util-app App with support for modules which allows for powerful feature testing of the application.

Scala Example

Given a module definition:

import com.twitter.inject.TwitterModule

object MyModule1 extends TwitterModule {

  @Singleton
  @Provides
  def providesFooService: FooService = ???
}

You could define an c.t.inject.app.App:

import com.google.inject.Module
import com.twitter.inject.Logging
import com.twitter.inject.app.App
import com.twitter.inject.modules.LoggingModule

object MyAppMain extends MyApp

class MyApp extends App with Logging  {

  override val modules: Seq[Module] = Seq(
    LoggingModule,
    MyModule1)

  override protected def run(): Unit = {
    // Core app logic goes here.
    val fooService = injector.instance[FooService]
    ???
  }
}

Tip

We include the c.t.inject.modules.LoggingModule to attempt installation of the SLF4JBridgeHandler here as an example of how to bridge legacy APIs.

Java Example

import java.util.Collection;
import java.util.Collections;

import com.google.inject.Module;

import com.twitter.inject.app.AbstractApp;
import com.twitter.inject.modules.LoggerModule$;

public class MyApp extends AbstractApp {

    @Override
    public Collection<Module> javaModules() {
        return Collections.<Module>singletonList(
                LoggerModule$.MODULE$,
                MyModule1$.MODULE$);
    }

    @Override
    public void run() {
        // Core app logic goes here.
        FooService fooService =
          injector().instance(FooService.class);
    }
}

Then create a “main” class:

final class MyAppMain {
    private MyAppMain() {
    }

    public static void main(String[] args) {
        new MyApp().main(args);
    }
}

See the Finatra examples for detailed examples with tests.

App#run

The single point of entry to the c.t.inject.app.App is the #run method.

The Finatra Startup Lifecycle ensures that the injector will be properly configured before access and provides the #run method as the function for implementing the app.

Using Injection

The c.t.inject.app.App exposes access to the configured c.t.inject.Injector which can be used to obtain instances from the object graph.

Note

You should take care to only access the injector in the App#run function as this is the correct place in the lifecycle where you are ensured to have a fully configured c.t.inject.Injector. Accessing the c.t.inject.Injector too early in the lifecycle (before it is configured) will result in an Exception being thrown.

An Example of Handling Signals

There may be cases where you want your application to handle an IPC signal instead of closing normally once the code execution is done, e.g., handling an INT (Ctrl-C) or TSTP (Ctrl-Z) signal.

You can use the com.twitter.util.HandleSignal utility to apply a callback to run on receiving the signal.

Note

Please consult the scaladocs for com.twitter.util.HandleSignal to make sure you are aware of the limitations of the code in handling signals.

For example, to exit the application upon receiving an INT signal:

import com.google.inject.Module
import com.twitter.inject.app.App
import com.twitter.inject.Logging

object MyAppMain extends MyApp

class MyApp extends App with Logging  {

  override val modules: Seq[Module] = Seq(
    MyModule1)

  HandleSignal("INT") { signal =>
    exitOnError(s"Process is being terminated with signal $signal.")
  }

  override protected def run(): Unit = {
    // Core app logic goes here.
    val fooService = injector.instance[FooService]
    ???
  }
}

Testing

First extend the c.t.inject.Test trait. Then to test, wrap your c.t.inject.app.App with an c.t.inject.app.EmbeddedApp.

For example,

import com.twitter.inject.Test
import com.twitter.inject.app.EmbeddedApp

class MyAppTest extends Test {

  // build an EmbeddedApp
  def app(): EmbeddedApp = app(new MyApp)
  def app(underlying: MyApp): EmbeddedApp =
    new EmbeddedApp(underlying).bind[Foo].toInstance(new Foo(2))

  test("MyApp#run") {
    app().main("username" -> "jack")
  }

  test("MyApp#works as expected") {
    // create a version of the app local to this test
    // here we could change the configuration of the app under test
    val localTestInstanceApp = app(new MyApp)
    localTestInstanceApp.main("username" -> "jill")

    // expect behavior against instances from the `localTestInstanceApp` injector
    localTestInstanceApp.injector.instance[Foo].value should be(Foo(2))
  }
}

and in Java:

import java.util.*;

import org.junit.Assert;
import org.junit.Test;

import com.twitter.inject.app.EmbeddedApp;

public class MyAppTest extends Assert {
    private EmbeddedApp app() {
        return this.app(new MyApp());
    }
    private EmbeddedApp app(MyApp underlying) {
        return new EmbeddedApp(underlying).bindClass(Foo.class, Foo.apply(2));
    }

    @Test
    public void testRun() {
      Map<String, Object> flags = new HashMap<String, Object>();
      flags.put("username", "jack");

      app().main(flags);
    }

    @Test
    public void testWorksAsExpected() {
      Map<String, Object> flags = new HashMap<String, Object>();
      flags.put("username", "jill");

      // create a version of the app local to this test
      // here we could change the configuration of the app under test
      MyApp localTestInstanceApp = app(new MyApp());
      app(localTestInstanceApp).main(flags);

      // expect behavior against instances from the `localTestInstanceApp` injector
      Foo foo = localTestInstanceApp.injector().instance(Foo.class);
      assertEquals(/* expected */ Foo.apply(2), /* actual */ foo);
    }
}

Important

Note: every call to EmbeddedApp#main will run the application with the given flags. If your application is stateful, you may want to ensure that a new instance of your application under test is created per test run, like written above.

There may be cases where you want to assert some metrics are written by your application for the purpose of testing functionality, even though your application may not export them for collection.

InMemoryStatsReceiver

Finatra prodives a StatsReceiverModule which will bind the LoadedStatsReceiver to the object graph.

Note

Finagle uses service-loading to allow for users to pick their com.twitter.finagle.stats.StatsReceiver implementation. We recommend using Twitter’s util-stats implementation which will be service-loaded as the implementation of LoadedStatsReceiver if the dependency is on your classpath.

Finagle provides an com.twitter.finagle.stats.InMemoryStatsReceiver which stores metrics in an in-memory Map to make it simple to query and assert their values. You can bind an instance of the InMemoryStatsReceiver to the underlying app’s object graph as the implementation of the app’s StatsReceiver when testing to be able to assert metrics emitted by the application.

Assuming you have an App:

  import com.google.inject.Module
  import com.twitter.finagle.stats.StatsReceiver
  import com.twitter.inject.Logging
  import com.twitter.inject.app.App
  import com.twitter.inject.modules.LoggingModule
  import com.twitter.inject.modules.StatsReceiverModule

  object MyAppMain extends MyApp

  class MyApp extends App with Logging  {

  override val modules: Seq[Module] = Seq(
    StatsReceiverModule
    LoggingModule,
    MyModule1)

  override protected def run(): Unit = {
    // Core app logic goes here.
    val statsReceiver = injector.instance[StatsReceiver]
    statsReceiver.counter("my_counter").incr()
    val fooService = injector.instance[FooService]
    ???
  }
}

You could then test emitted metrics like so:

import com.twitter.finagle.stats.InMemoryStatsReceiver
import com.twitter.inject.Test
import com.twitter.inject.app.EmbeddedApp

class MyAppTest extends Test {
  private val inMemoryStatsReceiver: InMemoryStatsReceiver = new InMemoryStatsReceiver

  // build an EmbeddedApp
  def app(): EmbeddedApp = app(new MyApp)
  def app(underlying: MyApp): EmbeddedApp =
    new EmbeddedApp(underlying)
      .bind[Foo].toInstance(new Foo(2))
      .bind[StatsReceiver].toInstance(inMemoryStatsReceiver)

  test("assert count") {
    val undertest = app()
    undertest.main("username" -> "jack")

    val statsReceiver = undertest.injector.instance[StatsReceiver].asInstanceOf[InMemoryStatsReceiver]
    statsReceiver.counter("my_counter")() shouldEqual 1
  }
}

For more information on the Embedded testing utilities, including on testing with GlobalFlags see the documentation here. Also see the documentation for more information on the #bind[T] DSL used above.

And lastly, for the complete testing guide, see the Testing table of contents.