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. We also recommend using Logback as your SLF4J implementation.
E.g., with sbt:
"com.twitter" %% "inject-app" % "24.2.0", "ch.qos.logback" % "logback-classic" % versions.logback,
For more information on logging with Finatra see: Introduction to Logging With Finatra.
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 = ???
/** Java-friendly way to access this module as a singleton instance */
def get(): this.type = this
}
You could define an c.t.inject.app.App:
import com.google.inject.Module
import com.twitter.inject.app.App
object MyAppMain extends MyApp
class MyApp
extends App {
override val modules: Seq[Module] = Seq(MyModule1)
override protected def run(): Unit = {
// Core app logic goes here.
val fooService = injector.instance[FooService]
???
}
}
Java Example¶
import java.util.Collection;
import java.util.Collections;
import com.google.inject.Module;
import com.twitter.inject.app.AbstractApp;
public class MyApp extends AbstractApp {
@Override
public Collection<Module> javaModules() {
return Collections.<Module>singletonList(MyModule1.get());
}
@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
object MyAppMain extends MyApp
class MyApp extends App {
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]
???
}
}
An Example of Handling Console Output¶
There may be cases where your application needs to output information to the console, such as when you are writing a command-line utility that prints its result when a computation has finished. There are some gotchas to making console output testable, so Finatra has exposed a ConsoleWriter that can be used by both Java and Scala applications. The ConsoleWriter avoids modifying global JVM state, as modifying System.setOut() and/or System.setErr() may result in flaky tests.
Note
Finatra exposes Logging integrations, which are a separate concern from the ConsoleWriter. The output of a Logger may not be appropriate for a command-line utility. The testing/binding of a Logger is currently beyond the scope of the ConsoleWriter. Additionally, Logger configuration is extremely flexible and there are no guarantees of messages reaching the console.
For more information on logging with Finatra see: Introduction to Logging With Finatra.
If your implementation is in Scala, the ConsoleWriter acts as a drop in replacement that allows for capturing any scala.Console output (i.e. via println()) within the c.t.inject.app.App#run() method via the TestConsoleWriter without explicit use of the ConsoleWriter.
For example,
import com.twitter.inject.app.App
import com.twitter.inject.console.ConsoleWriter
class MyApp extends App {
override def run(): Unit = {
val console = injector.instance[ConsoleWriter]
console.out.println("Hello, World!")
console.err.println("Oops!")
// println("Hello, World!") will be forwarded to `console`
// Console.out.println("Hello, World!") will be forwarded to `console`
// System.out.println("Hello, World!") will NOT be forwarded to `console`
}
}
If your implementation is in Java, the System.out and System.err calls will not be forwarded to the ConsoleWriter automatically, due to their previously mentioned coupling to global JVM state. For Java users, it would be preferred to have expected console output of the c.t.inject.app.App#run() method use the ConsoleWriter directly.
For example:
import com.twitter.inject.app.AbstractApp;
import com.twitter.inject.console.ConsoleWriter;
public class MyApp extends AbstractApp {
@Override
public void run() {
// Core app logic goes here.
ConsoleWriter console =
injector().instance(ConsoleWriter.class);
console.out().println("Hello, World!");
console.err().println("Oops!");
// System.out.println("Hello, World!") will NOT be forwarded to `console`
}
}
See TestConsoleWriter for testing and verifying ConsoleWriter output.
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.
TestConsoleWriter¶
Finatra provides a ConsoleWriterModule which will bind the ConsoleWriter to the object graph. The ConsoleWriter can be bound for testing with the TestConsoleWriter in order to inspect the console output of a command-line style App.
Assuming you have an App:
import com.twitter.inject.app.App
import com.twitter.inject.app.console.ConsoleWriter
object MyAppMain extends MyApp
class MyApp extends App {
override protected def run(): Unit = {
// Core app logic goes here.
val console = injector.instance[ConsoleWriter]
console.out.println("Hello, World!")
// output to the Scala `Console` will also be captured, the following would be equivalent:
// Console.out.println("Hello, World!")
// println("Hello, World!")
}
}
and in Java:
import com.twitter.inject.app.AbstractApp;
import com.twitter.inject.console.ConsoleWriter;
public class MyApp extends AbstractApp {
@Override
public void run() {
// Core app logic goes here.
ConsoleWriter console =
injector().instance(ConsoleWriter.class);
console.out().println("Hello, World!");
// System.out.println("Hello, World!"); will NOT be captured for testing
}
}
You could then test emitted console output like so:
import com.twitter.inject.Test
import com.twitter.inject.app.EmbeddedApp
import com.twitter.inject.app.TestConsoleWriter
import com.twitter.inject.app.console.ConsoleWriter
class MyAppTest extends Test {
val console = new TestConsoleWriter()
// build an EmbeddedApp
def app(): EmbeddedApp = app(new MyApp).bind[ConsoleWriter].toInstance(console)
override def beforeEach(): Unit = {
console.reset()
super.beforeEach()
}
test("assert count") {
val undertest = app()
undertest.main()
assert(console.inspectOut() == "Hello, World!\n")
}
}
and in Java:
import com.twitter.inject.app.EmbeddedApp;
import com.twitter.inject.app.TestConsoleWriter;
import com.twitter.inject.app.console.ConsoleWriter;
import junit.Assert;
import junit.Test;
public class MyAppTest extends Assert {
@Test
public void testOutput() throws Exception {
TestConsoleWriter console = new TestConsoleWriter();
EmbeddedApp app = new EmbeddedApp(myApp).bindClass(ConsoleWriter.class, console);
app.main();
assertEquals(console.inspectOut(), "Hello, World!\n");
}
}
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.app.App
import com.twitter.inject.modules.StatsReceiverModule
object MyAppMain extends MyApp
class MyApp extends App {
override val modules: Seq[Module] = Seq(
StatsReceiverModule
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.