Node.js で書いたAzure Functions アプリのテストを書く。
自分で使うためだけのTwitterクライアントをほそぼそと開発していて、それのテストがそろそろ欲しいと思い、Jest を使って書いてみた。 手始めに、Azure Functions アプリのテストを書いてみたので、それについてまとめてみる。
実際のコードは、こちら。
Jest
JestはFacebookが開発するJavaScriptのテストツール。Node.jsで書いたライブラリやフロントのアプリのテストも同じように書ける。 知人に勧められて使ってみることにしたのが発端ではあるが、今開発しているものはAPIはAzure Functions(Node.js)で、フロントがVue.jsで開発しているので、ちょうど良いと思った。
あとで気づいたのですが、公式ドキュメントもJestでテストの説明が書いてあります。
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
メソッドを呼んでいる。異常終了させたい場合は、この引数にエラーメッセーや例外オブジェクトを渡す。
ということで、大まかなテストの方針として、以下のように考える。
それぞれについて書いていく。
前提条件を作る。
context
は、
- バインディングの情報を持つ
bindings
プロパティ - ログ出力のための
log
オブジェクト - 終了通知のための
done
メソッド
を持つオブジェクトなので、それぞれを再現させるようにモックなどを使う。
入力バインディングは、Functionsアプリの引数としても渡すことができるので、状況に応じて使い分ける。
bindings
は出力を受け付けるためにも使うので、入力バインディングが引数だとしても用意する。
log
やdone
は、関数内部で呼ぶことさえできればいいので、モック関数を充てる。
上の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.res
のstatus
には、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) })
また、ログに期待したものが書き込まれているかや、正しくエラーを検知して異常終了できているか確認したければ、log
やdone
が呼ばれたかどうか、その時の引数についてチェックすると良いだろう。
expect(log).toHaveBeenCalledWith('Error found!')
expect(done).toHaveBeenCalledWith(expectedErrorObject)
おわり
Azure Functions は、Blobストレージなど連携するサービスをバインディングを定義することで、SDKなど準備する必要なく利用できるところが便利で、そのバインディングのおかげで関数の実装だけでなくテストも簡易に書ける。
あとは、function.jsonで定義されているバインディングの名前と合致してるかとか確認できれば、デプロイ前のテストとしては良さそう。