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:
-
Once all paths have been completed, label is set on the corresponding pull request: and a message is written to logs
All tests in production passed.
- Add library to your application's dependencies:
libraryDependencies += "com.gu" %% "tip" % "0.6.4"
- 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
- Instantiate
Tip
withTipConfig
:val tipConfig = TipConfig(repo = "guardian/identity", cloudEanbled = false) TipFactory.create(tipConfig)
- Call
tip.verify("My Path Name"")
at the point where you consider path has been successfully completed.
- Instantiate
Tip
withTipConfig
(which by default enables cloud)::val tipConfig = TipConfig("guardian/identity") TipFactory.create(tipConfig)
- Call
tip.verify("My Path Name"")
at the point where you consider path has been successfully completed. - Access board at
<tip cloud domain>/{owner}/{repo}/boards/head
to monitor verification in real-time.
Optionally, if you want Tip to notify when all paths have been hit by setting a label on the corresponding merged PR, then
- Create a GitHub label, for instance, a green label with name
Verified in PROD
: - Create a GitHub personal access token with at least
public_repo
scope. Keep this secret! - Set
personalAccessToken
inTipConfig
:TipConfig( repo = "guardian/identity", personalAccessToken = some-secret-token )
Optionally, if you want to have a separate board for each merged PR, then
- Set
boardSha
inTipConfig
:TipConfig( repo = "guardian/identity", boardSha = some-sha-value )
- Example Tip configuration which uses
sbt-buildinfo
to setboardSha
:build.sbt: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 )
buildInfoKeys := Seq[BuildInfoKey]( BuildInfoKey.constant("GitHeadSha", "git rev-parse HEAD".!!.trim) ) buildInfoPackage := "com.gu.identity.api" buildInfoOptions += BuildInfoOption.ToMap )
- 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 Future
s
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.