AppContext: how and when to use it
We want our services to be composable, so that one service can call another. We also want to be able to have a per request trace id associated with a logger so that we can correlate log messages in a single request.
See ADR 0064 Use Stateless Services with Context for more background.
The way we achieve that goal is by storing the DB connection, logger, and
session inside an instance of appcontext.AppContext
, and requiring all
functions that need one or more of those elements (DB, logger, session) to accept an argument of type appcontext.AppContext
as its first argument. The function can then extract those elements from the app context, like so:
func MyFunction(appCtx appcontext.AppContext) {
db := appCtx.DB()
logger := appCtx.Logger()
session := appCtx.Session()
}
Search the mymove
codebase for appCtx appcontext.AppContext
for more usage examples. We also have examples here in
Service Objects
and Guide to Creating an Endpoint.
Testing with AppContext
When testing a service object function, we need to pass in an instance of the AppContext that contains the test DB and test logger. That instance can be created like this:
appcontext.NewAppContext(suite.DB(), suite.logger)
Since this is a common thing to do in tests, packages should extract this into a TestAppContext()
helper method that's defined in the package's *_test.go
file. For example, it is defined in pkg/services/mto_shipment/mto_shipment_service_test.go
for the mtoshipment
package:
// TestAppContext returns the AppContext for the test suite
func (suite *MTOShipmentServiceSuite) TestAppContext() appcontext.AppContext {
return appcontext.NewAppContext(suite.DB(), suite.logger)
}
Search for TestAppContext() appcontext.AppContext
in the codebase for more examples.
You can then use it in tests like this:
func (suite *MTOShipmentServiceSuite) TestRejectShipment() {
router := NewShipmentRouter()
approver := NewShipmentRejecter(router)
reason := "reason"
suite.T().Run("If the shipment rejection is approved successfully, it should update the shipment status in the DB", func(t *testing.T) {
shipment := testdatagen.MakeDefaultMTOShipmentMinimal(suite.DB())
shipmentEtag := etag.GenerateEtag(shipment.UpdatedAt)
fetchedShipment := models.MTOShipment{}
rejectedShipment, err := approver.RejectShipment(suite.TestAppContext(), shipment.ID, shipmentEtag, &reason)
suite.NoError(err)
suite.Equal(shipment.MoveTaskOrderID, rejectedShipment.MoveTaskOrderID)
err = suite.DB().Find(&fetchedShipment, shipment.ID)
suite.NoError(err)
suite.Equal(models.MTOShipmentStatusRejected, fetchedShipment.Status)
suite.Equal(shipment.ID, fetchedShipment.ID)
suite.Equal(rejectedShipment.ID, fetchedShipment.ID)
suite.Equal(&reason, fetchedShipment.RejectionReason)
})
}