自分で使うためだけのTwitterクライアントをほそぼそと開発していて、それのテストがそろそろ欲しいと思い、Jest を使って書いてみた。 手始めに、Azure Functions アプリのテストを書いてみたので、それについてまとめてみる。

実際のコードは、こちら。

github.com

Jest

jestjs.io

JestはFacebookが開発するJavaScriptのテストツール。Node.jsで書いたライブラリやフロントのアプリのテストも同じように書ける。 知人に勧められて使ってみることにしたのが発端ではあるが、今開発しているものはAPIはAzure Functions(Node.js)で、フロントがVue.jsで開発しているので、ちょうど良いと思った。

あとで気づいたのですが、公式ドキュメントもJestでテストの説明が書いてあります。

docs.microsoft.com

Azure Functions アプリのテスト

Azure Functions Core Toolsを使えば、Azure上で動いているのと同じようにローカルの開発環境でAzure Functionsアプリを動かすことができる。 しかし、それでも、再現が面倒臭いものがあったり、手作業でテストする事自体が面倒。 例えば、バインディングしているBlobストレージから得られる情報に応じての振る舞いをテストしようとしたり、バインディングでサポートされていない外部のAPIへのアクセスなどをモックしたりするのは面倒くさい。 また、今回開発しているアプリではWebApps のEasyAuthという認証認可の仕組みを使っている。 それを再現するためには、認証情報を表した特殊なHTTPヘッダを追加しないといけない、といった面倒くささがある。

方針

Functionsアプリそれぞれは、context と入力バインディングを引数とした関数として記述される。 例えば、HTTPトリガーの関数の場合、

module.exports = async function(context, req) {
    const name = req.query.name;
    context.bindings.res = { status: 200, body: `Hello, {name} !` }
    done()
}

のように、引数としてcontext とHTTPリクエストを入力バインディングreqとして受け取る。HTTPレスポンスなどFunctionsアプリの出力は、出力バインディングを経由して出力する(上の例の場合、context.bindings.res)。 また終了したこと表すため、doneメソッドを呼んでいる。異常終了させたい場合は、この引数にエラーメッセーや例外オブジェクトを渡す。

ということで、大まかなテストの方針として、以下のように考える。

  • 前提条件として、入力バインディングに値、オブジェクトを設定し、
  • それを引数としてテスト対象のFunctionsアプリに与え、実行し、
  • 出力バインディングが期待した状態になっているかチェックする。

それぞれについて書いていく。

前提条件を作る。

context は、

  • バインディングの情報を持つbindings プロパティ
  • ログ出力のためのlogオブジェクト
  • 終了通知のためのdoneメソッド

を持つオブジェクトなので、それぞれを再現させるようにモックなどを使う。 入力バインディングは、Functionsアプリの引数としても渡すことができるので、状況に応じて使い分ける。 bindingsは出力を受け付けるためにも使うので、入力バインディングが引数だとしても用意する。 logdoneは、関数内部で呼ぶことさえできればいいので、モック関数を充てる。

上のHTTPトリガーの関数の場合、引数として入力バインディングを受け取り、出力はbindingsを経由して出力するので、下記のように前提を作る。

test('should response 200', async () => {
  const bindings = {}
  const req = {
     query: { name: 'satoryu' }
  }
  const log = jest.fn() 
  const done = jest.fn()

  // TODO: Call Function

  // TODO: Check output bindings
})

関数を実行する。

function core toolsなどテンプレートから作ったFunctionsアプリは、通常、関数をexportしたJavaScriptファイルなので、それをテストコード内でrequireし、その関数に上で用意したbindingsなどを引数に渡すことで実行できる。

const httpTriggerFunction = require('../HttpTrigger')

test('should response 200', async () => {
  const bindings = {}
  const req = {
     query: { name: 'satoryu' }
  }
  const log = jest.fn() 
  const done = jest.fn()

  // Call Function
  await httpTriggerFunction({ bindings, log, done }, req)

  // TODO: Check Output Bindings
})

出力をチェックする。

あとは、関数によってbindingsに追加された出力バインディングへの値をチェックするだけ。 例えば、bindings.resstatusには、200が入っているはずなので、それを確認する。

const httpTriggerFunction = require('../HttpTrigger')

test('should response 200', async () => {
  const bindings = {}
  const req = {
     query: { name: 'satoryu' }
  }
  const log = jest.fn() 
  const done = jest.fn()

  // Call Function
  await httpTriggerFunction({ bindings, log, done }, req)

  //Check Output Bindings
  expect(bindings.res.status).toBe(200)
})

また、ログに期待したものが書き込まれているかや、正しくエラーを検知して異常終了できているか確認したければ、logdoneが呼ばれたかどうか、その時の引数についてチェックすると良いだろう。

expect(log).toHaveBeenCalledWith('Error found!')
expect(done).toHaveBeenCalledWith(expectedErrorObject)

おわり

Azure Functions は、Blobストレージなど連携するサービスをバインディングを定義することで、SDKなど準備する必要なく利用できるところが便利で、そのバインディングのおかげで関数の実装だけでなくテストも簡易に書ける。

あとは、function.jsonで定義されているバインディングの名前と合致してるかとか確認できれば、デプロイ前のテストとしては良さそう。