Startup Tests¶
Important
Please see the section on including test-jar dependencies in your project: Test Dependencies.
By default the Finatra embedded testing infrastructure sets the Guice com.google.inject.Stage to DEVELOPMENT for the object graph of the server or application under test. For purposes of testing we choose the trade-off of a fast start-up time for the embedded server at the expense of some runtime performance as classes are lazily loaded when accessed by the test features.
However, this also means that if you have mis-configured dependencies (e.g., you attempt to inject a type that the injector cannot construct because it does not have a no-arg constructor nor was it provided by a module) you may not run into this with regular FeatureTesting as dependencies are satisfied lazily by default.
As such, we recommend creating a test – a StartupTest to check that your service can start up and report itself as healthy. This is useful for checking the correctness of the object graph, catching errors that could otherwise cause the server to fail to start.
Best Practices¶
A StartupTest should mimic production as closely as possible.
The com.google.inject.Stage SHOULD be set to PRODUCTION so that all singletons will be eagerly created at startup (Stage.DEVELOPMENT is set by default).
Avoid replacing any bound types (do not use Explicit Binding with #bind[T] or Override Modules).
Prevent any Finagle clients from making outbound connections during startup by setting the resolution of clients to nil. See: Disabling Client using Dtabs.
For example:
import com.google.inject.Stage
import com.twitter.finatra.http.EmbeddedHttpServer
import com.twitter.inject.server.FeatureTest
import scala.collection.immutable.ListMap
class MyServiceStartupTest extends FeatureTest {
val server = new EmbeddedHttpServer(
stage = Stage.PRODUCTION,
twitterServer = new SampleApiServer,
globalFlags = ListMap(com.some.globalFlag.disable -> "true"),
flags = Map(
"dtab.add" -> "/$/inet=>/$/nil;/zk=>/$/nil")
)
test("SampleApiServer#startup") {
server.assertHealthy()
}
}
Tip
This works for EmbeddedHttpServer or EmbeddedThriftServer as well because #assertHealthy()
is defined on the EmbeddedTwitterServer super class.
For examples of how to accomplish this in Java, see HelloWorldServerStartupTest.
c.t.server.TwitterServer¶
As seen previously, Finatra’s c.t.inject.server.FeatureTest utility can be used for testing any extension of c.t.server.TwitterServer – the server under test does not specifically have to be a server written using the Finatra framework.
Here is an example of writing a StartupTest that starts a c.t.server.TwitterServer and asserts that it reports itself “healthy”.
Given a simple c.t.server.TwitterServer:
import com.twitter.finagle.{Http, ListeningServer, Service}
import com.twitter.finagle.http.{Status, Response, Request}
import com.twitter.server.TwitterServer
import com.twitter.util.{Await, Future}
class MyTwitterServer extends TwitterServer {
private[this] val httpPortFlag =
flag(name = "http.port", default = ":8888", help = "External HTTP server port")
private[this] def responseString: String = "Hello, world!"
private[this] val service = Service.mk[Request, Response] { request =>
val response =
Response(request.version, Status.Ok)
response.contentString = responseString
Future.value(response)
}
/** Simple way to expose the bound port once the external listening server is started */
@volatile private[this] var _httpExternalPort: Option[Int] = None
def httpExternalPort: Option[Int] = this._httpExternalPort
def main(): Unit = {
val server: ListeningServer = Http.server
.withLabel("http")
.serve(httpPortFlag(), service)
info(s"Serving on port ${httpPortFlag()}")
info(s"Serving admin interface on port ${adminPort()}")
onExit {
Await.result(server.close())
}
this._httpExternalPort = Some(server.boundAddress.asInstanceOf[InetSocketAddress].getPort)
Await.ready(server)
}
}
For examples of how to accomplish this in Java, see ExampleTwitterServerStartupTest.
Writing the StartupTest¶
First, extend the c.t.inject.server.FeatureTest trait. Then override the server definition with an instance of your EmbeddedTwitterServer which wraps your c.t.server.TwitterServer under test.
import com.twitter.inject.server.{EmbeddedTwitterServer, FeatureTest, PortUtils}
import scala.collection.immutable.ListMap
class MyTwitterServerFeatureTest extends FeatureTest {
override protected val server =
new EmbeddedTwitterServer(
twitterServer = new MyTwitterServer,
globalFlags = ListMap(com.some.globalFlag.disable -> "true"),
flags = Map(
"http.port" -> PortUtils.ephemeralLoopback,
"dtab.add" -> "/$/inet=>/$/nil;/zk=>/$/nil"
)
)
test("MyTwitterServer#starts") {
server.isHealthy should be(true)
}
}
It is important to note that for a “non-injectable” TwitterServer, i.e., a direct extension of c.t.server.TwitterServer, the above testing assumes that many of your service startup issues can be determined at class construction, or in the init or premain lifecycle phases.
Why?
By default, the EmbeddedTwitterServer will start the underlying server in an different thread, then wait for the server to start before allowing a test to proceed. However, this differs when the underlying server is a c.t.server.TwitterServer vs. when it is a c.t.inject.server.TwitterServer.
For a c.t.server.TwitterServer the EmbeddedTwitterServer has no hook to determine if a server has fully started, so relies solely on the HTTP Admin Interface reporting itself as healthy. Note, therefore, if you configure your server to disable the TwitterServer HTTP Admin Interface, then you will not be able to test your server in this manner as the framework will have no way to determine when the server has started.
For a c.t.inject.server.TwitterServer the EmbeddedTwitterServer is able to wait for the server to report itself as “started” in the c.t.inject.app.App#main.
Thus, testing your server is healthy for a c.t.server.TwitterServer is merely a check against the HTTP Admin Interface which is started in the premain phase.
Caution
If all of your c.t.server.TwitterServer logic is contained in the main of your server (like Finagle client creation, external ListeningServer creation, etc), it is very possible when the server under test is started in a separate thread, the TwitterServer HTTP Admin Interface will start and report that it is healthy, then the test process will exit before the server under test in the other thread has gotten to executing its main method and thus exiting before exercising any logic.
In cases like this, you should also ensure to test the logic of your server in regular FeatureTests and not only assert it is reported as healthy. Again, see the documentation on the Application and Server Lifecycle for more information.