gatlingのレスポンスボディをCirceでデコードする

2019.06.18

はじめに

GatlingでAPIテストを記述しているとリクエストボディのJSONに対するアサーションを記述する場合があります。公式にはJSON PathでJSONの中身を取り出すAPIが提供されています。

Gatling Checks

こんな感じです。

// JSON Response
{
  "foo": 1,
  "bar" "baz"
}

jsonPath("$..foo").ofType[Int] // will match 1

Circeでパースしたい

Scalaを使っているとCirceでVOにマッピングしたくなるのが人情です。やってみます。

こうなる

そして、いきなりですが、こうなります。(コード全体はこちらにあります)

object CirceOps {

  type ParseResult[A] = Either[Throwable, A]
  type RequestBodyType = Option[String]

  //Either[Throwable, A] => Validation[A]の変換
  private val parseResultToValidation = new FunctionK[ParseResult, Validation] {
    override def apply[A](fa: ParseResult[A]): Validation[A] = fa.fold(l => Failure(l.getMessage), r => Success(r))
  }

  //パース(Option[String] => Either[Throwable, Option[A]])
  private def parse[A: Decoder]: Kleisli[ParseResult, RequestBodyType, Option[A]] = Kleisli[ParseResult, RequestBodyType, Option[A]] { (a: RequestBodyType) =>
    a match {
      case Some(s) => parseJson(s).flatMap(_.as[A].map(_.some))
      case None => none[A].asRight
    }
  }


  /**
    * リクエストボディをAとしてパースする
    * @param bodyString
    * @tparam A
    * @return
    */
  def parseRequestBody[A: Decoder](bodyString: RequestBodyType): Validation[Option[A]] = parse.mapK(parseResultToValidation).run(bodyString)


}

使い方

JSONは以下のようなcase classにマッピングされるとします。

final case class Greeting(greeting: String) extends AnyVal

object Greeting {
    implicit val greetingEncoder: Encoder[Greeting] = (a: Greeting) => Json.obj(
          ("message", Json.fromString(a.greeting)),
        )
    implicit val greetingDecoder: Decoder[Greeting] = (c: HCursor) => c.downField("message").as[String].map(Greeting.apply)
}

こんな感じでbodyString.transformOptionの引数で渡す関数としてCirceOps#parseRequestBodyを指定します。

val sc = scenario("Hello").exec(
    http("GET /hello/myName")
      .get("/hello/myName")
      //パースしてGreetingのインスタンスと比較
      .check(
        bodyString.transformOption(parseRequestBody[Greeting](_)).is((Greeting("Hello, myName")))
      )
  )

何をしているのか

このサンプルでは、transformOptionの定義に従って、Option[String] => Validation[Option[A]] の変換を行っています。 Circeでのパースで間にEitherが挟まる関係で次のように2段階に分けて変換を行っています。

  1. Option[String] => Either[Throwable, Option[A]]
  2. Either[Throwable, Option[A]] => Validation[Option[A]]

この例ではcatsを使いましたが、上記の変換を行うメソッドを普通に実装しても変換は行えると思います。

まとめ

gatlingでレスポンスのボディのアサーションを型安全に行えるようになりました。 refined なども併用することでJSONのデコード時に値の正当性などもチェックできるので組み合わせてみるのもいいと思います。