Testing in Production (TiP)

How to verify user journeys are not broken without writing a single test?

  • First time a production user completes a path the corresponding square on the board lights up green: board_example

  • Once all paths have been completed, label is set on the corresponding pull request: pr_label_example and a message is written to logs All tests in production passed.

User Guide

Tip.verify

  1. Add library to your application's dependencies:
    libraryDependencies += "com.gu" %% "tip" % "0.6.4"
    
  2. List paths to be covered in tip.yaml file and make sure it is on the classpath:
    - name: Register
      description: User creates an account
    
    - name: Update User
      description: User changes account details
    
  3. Instantiate Tip with TipConfig:
    val tipConfig = TipConfig(repo = "guardian/identity", cloudEanbled = false)
    TipFactory.create(tipConfig)
  4. Call tip.verify("My Path Name"") at the point where you consider path has been successfully completed.

Configuration with cloud enabled - single board

  1. Instantiate Tip with TipConfig (which by default enables cloud)::
    val tipConfig = TipConfig("guardian/identity")
    TipFactory.create(tipConfig)
  2. Call tip.verify("My Path Name"") at the point where you consider path has been successfully completed.
  3. Access board at <tip cloud domain>/{owner}/{repo}/boards/head to monitor verification in real-time.

Setting a label on PR

Optionally, if you want Tip to notify when all paths have been hit by setting a label on the corresponding merged PR, then

  1. Create a GitHub label, for instance, a green label with name Verified in PROD: label_example
  2. Create a GitHub personal access token with at least public_repo scope. Keep this secret!
  3. Set personalAccessToken in TipConfig:
    TipConfig(
      repo = "guardian/identity",
      personalAccessToken = some-secret-token
    )

Board by merge commit SHA

Optionally, if you want to have a separate board for each merged PR, then

  1. Set boardSha in TipConfig:
    TipConfig(
      repo = "guardian/identity",
      boardSha = some-sha-value
    )
    
  2. Example Tip configuration which uses sbt-buildinfo to set boardSha:
    TipConfig(
      repo = "guardian/identity",
      personalAccessToken = config.Tip.personalAccessToken, // remove if you do not need GitHub label functionality
      label = "Verified in PROD", // remove if you do not need GitHub label functionality
      boardSha = BuildInfo.GitHeadSha // remove if you need only one board instead of board per sha
    )
    build.sbt:
      buildInfoKeys := Seq[BuildInfoKey](
        BuildInfoKey.constant("GitHeadSha", "git rev-parse HEAD".!!.trim)
      )
      buildInfoPackage := "com.gu.identity.api"
      buildInfoOptions += BuildInfoOption.ToMap
    )
  3. Access board at <tip cloud domain>/board/{sha}

TipAssert runs an assertion on a pass-by-name value and simply logs an error on failed assertion. The idea is to have assertions run on production behaviour off the main thread in a separate execution context which should not affect main business logic, whilst being used in combination with crash monitoring software (for example, Sentry) which can alert on log.error statement.

Users should use a separate ExecutionContext dedicated just to assertions to make sure assertions are not starving main business logic thread pool:

import com.gu.tip.assertion.ExecutionContext.assertionExecutionContext
Future(/* request we will assert on */)(assertionExecutionContext)

(Note Because ExecutionContext of a Future cannot be changed after Future definition, TiP cannot take care of this for the user.)

For example, say we have a scenario where we take payments from user and want to make sure we have not double charged them. Given the following requests returning Futures

def chargeUser(implicit ec: ExecutionContext): Future[_]
def getNumberOfCharges(implicit ec: ExecutionContext): Future[Int]

then we could check if user has been double charged with

import com.gu.tip.assertion.TipAssert
import com.gu.tip.assertion.ExecutionContext.assertionExecutionContext

chargeUser(mainExecutionContext) andThen { case _ =>
  TipAssert(
    getNumberOfCharges(assertionExecutionContext),
    (num: Int) => num == 1,
    "User should be charged only once. Fix ASAP!"
  )
}

TipAssert can also handle eventually semantics via max and delay parameters for scenarios where database mutation is only eventually consistent. Here is the full signature:

def apply[T](
     f: => Future[T],
     p: T => Boolean,
     msg: String,
     max: Int = 1,
     delay: FiniteDuration = 0.seconds
 ): Future[AssertionResult] 

Note currently TipAssert is not related to Tip.verify functionality in any way. One major semantic difference between the two is that TipAssert checks failed paths whilst Tip.verify checks successful paths.

Releasing latest version of the library

See How to make a release