AWS
Kotlin
CircleCI
mailtrap
OriginalトレタDay 14

e2eテスト自動化する

この記事はトレタ Advent Calendar 2017 14日目の記事です。

ウェブサイトでやらなきゃいけないことナンバーワンだけどやっていないことナンバーワンのe2eを自動化した話をしたいと思います。

トレタでは飲食店向けの予約サイトをウェブで公開できる機能が付いています。
サービス開始から4年経っていますので様々な機能や設定が当初より増えてまいりました。
それらを毎回手動e2eは大変で忘れてしまうことがあるので、定期実行&ビルド時の自動e2eテストを作成する必要ができました。

ここで問題になったのが予約完了メールでした。
サイトと予約サーバは別であり、送信されるメールは事前にわからなかったのです。

そこで利用したサービスが
スクリーンショット 2017-12-14 14.44.32.png

mailtrapになります。

こちらは受信箱に送られてきたメールをAPI経由で取得できるというサービスになります。
無料ではダミーsmtpサーバとして動き、送信する予定のメールを取得します。
有料(Bee-trap)にしますと、Mailtrapでメールアドレスを発行でき、smtpをMailtrapに向けなくても利用することが可能となります。
スクリーンショット 2017-12-14 14.51.16.png

スクリーンショット 2017-12-14 14.46.07.png
メールが着弾するとinboxに溜まっていきます。

それをAPI経由で取得して、テストします。

API.ts
app.use('/mailtrap/lastMessage', (req, res) => {
  const mailTrap = new Mailtrap();
  mailTrap.getInboxFromName('inbox', (inbox) => {
    mailTrap.getMessages(inbox.id, (messages) => {
      if (messages.length > 0) {
        const message = messages[messages.length - 1];
        mailTrap.getMessageTxt(message.inbox_id, message.id, (msg) => {
          mailTrap.deleteMessage(message.inbox_id, message.id, () => {
            res.status(200).json(msg);
          });
        });
      } else {
        res.status(200).json({});
      }
    });
  });
});

これをprotoractor経由で取得し、メールを確認します。
(specファイルは長いので割愛)

これで、懸念の一つはなくなりました。
次はどのように定期実行させるかです。
e2e自体はCicleCI上で実行させています。

そこで、e2eのブランチを定期的にrebuildさせて、回すことにしました。
それでAWSのlambdaでCloudWatch 4hで実行させることにしました。

最後に成功したビルドをリビルドします。
あっ、ちなみにkotlinで書いております。

CircleCIE2E.kt
package `in`.toreta.e2e

import `in`.toreta.common.DefaultObserver
import com.github.unhappychoice.circleci.CircleCIAPIClient
import com.unhappychoice.norimaki.model.Build
import com.unhappychoice.norimaki.model.Project
import rx.Observable

class CircleCIE2E {
  val circleCIToken = System.getenv("circle_ci_token")
  val circle = CircleCIAPIClient(circleCIToken).client()
  val userName = "toreta"
  val projectName = "toreta-web-reservation"
  var buildNum = 0
  var data: Any? = null
  var isSucces: Boolean = false
  fun run(): Observable<Build> {
    return Observable.create {
      val subscriber = it
      circle.getProjects().subscribe(DefaultObserver<List<Project>>(next = {
        val project = it.filter {
          project ->
          project.vcsUrl.equals("https://github.com/repository")
        }[0]
        project.also {
          it.branches["e2e"].also {
            val branch = Map::class.javaObjectType.cast(it)
            if (branch["last_success"] != null) {
              branch["last_success"].also {
                val lastSuccess = Map::class.javaObjectType.cast(it)
                println(lastSuccess)
                buildNum = Double::class.javaObjectType.cast(lastSuccess["build_num"]).toInt()
                println(buildNum)
              }
            }
          }
        }
        circle.retryBuild(userName, projectName, buildNum).subscribe(DefaultObserver<Build>(
            next = {
              isSucces = true
              data = it
              subscriber.onNext(it)
              subscriber.onCompleted()
            },
            error = {
              isSucces = false
              data = it
              subscriber.onError(it)
              subscriber.onCompleted()
            }
        )

        )
      }))
    }
  }
}

スクリーンショット 2017-12-14 16.07.11.png

それをAPI Gateway経由で実行します。

Main.kt
package e2e
import `in`.toreta.common.DefaultObserver
import `in`.toreta.e2e.CircleCIE2E
import `in`.toreta.e2e.GitHubToretaWebReservation
import com.amazonaws.services.lambda.runtime.Context
import com.amazonaws.services.lambda.runtime.RequestHandler
import com.beust.klaxon.JsonObject
import com.beust.klaxon.json
import com.unhappychoice.norimaki.model.Build
import java.util.concurrent.CountDownLatch
import kotlin.concurrent.thread


class APIRequest {
  var s = ""
}


class APIHandler: RequestHandler<APIRequest, JsonObject> {
  override fun handleRequest(input: APIRequest, context: Context): JsonObject {
    if (input.s == "c") {
      val circleCIE2E = CircleCIE2E()
      val latch = CountDownLatch(1)
      thread {
        circleCIE2E.run().subscribe(DefaultObserver<Build>(
            next = {
              latch.countDown()
            },
            error = {
              latch.countDown()
            }
        ))
      }
      latch.await()
      val result = json {
        obj(
            if (circleCIE2E.isSucces) ("status" to "success") else ("status" to "error")
        )
      }
      return result
    } else {
      val result = json {
        obj ("status" to "error", "message" to "サービスを選択せよ!!")
      }
      return result
    }
  }
}

fun main(args: Array<String>) {
  val github = GitHubToretaWebReservation()
  github.merge()
  println(github.data)
}

結果をSlackに流して完了となります。
4時間に一度自動実行できるようになりました。

まとめ

  • e2eはCircleCIで回せると良い。
  • 定期実行はCloudWatchが便利。
  • メールテストはmailtrapがおすすめ。
  • kotlinが使いたかったから使ってみた。