Fastly Compute と DevOps, CI/CD (1) Rust/Go/JS 各 SDK でのテストの書き方とプラクティス
この記事は Fastly Compute 一人アドベントカレンダー 2024 17 日目の記事です。本稿では Compute と DevOps(CI/CD) という切り口のシリーズ第一回目として、Rust/Go/JS 各 SDK を使った結合テストや E2E テストの書き方やプラクティスを紹介します。
結合テスト
結合テストは単体テストに近いレベルの粒度の細かなものから、E2E テストに近いような粒度の大きいものまで様々な結合テストが検討できますが、ここでは手始めに初手で考えやすい後者の粒度の大きい結合テストを考えます。すなわち、以下のような引数で受け付けた Request 変数について処理を行う hanlder 関数に対してテストを記述する方法について、各 SDK での実装を見ていきたいと思います。
fn main() -> Result<(), Error> {
let ds_req = Request::from_client();
let us_resp = handler(ds_req)?;
us_resp.send_to_client();
Ok(())
}
fn handler(req: Request) -> Result<Response, Error> {
...
Rust のコード例
Rust の SDK を使ったプロジェクトの E2E テストの書き方について、参考になるブログ記事がありますので紹介します。この記事では、 Request 変数を引数に取り Result<Response, Error> 型の戻り値を返す関数 handler
を定義しておき、この関数に対して assert_eq!()
でテストを書くことで結合テストを実現するアプローチが紹介されています。
#[test]
fn test_post() {
let req = fastly::Request::post("http://example.com/");
let resp = handler(req).expect("request succeeds");
assert_eq!(resp.get_status(), StatusCode::METHOD_NOT_ALLOWED);
assert_eq!(resp.get_content_type(), Some(mime::TEXT_PLAIN_UTF_8));
assert_eq!(resp.into_body_str(), "This method is not allowed\n");
}
#[test]
fn test_homepage() {
let req = fastly::Request::get("http://example.com/");
let resp = handler(req).expect("request succeeds");
assert_eq!(resp.get_status(), StatusCode::OK);
assert_eq!(resp.get_content_type(), Some(mime::TEXT_HTML_UTF_8));
assert!(resp.into_body_str().contains("Welcome to Compute@Edge"));
}
#[test]
fn test_missing() {
let req = fastly::Request::get("http://example.com/missing");
let resp = handler(req).expect("request succeeds");
assert_eq!(resp.get_status(), StatusCode::OK);
assert_eq!(resp.get_content_type(), Some(mime::TEXT_PLAIN_UTF_8));
assert_eq!(
resp.into_body_str(),
"The page you requested could not be found\n"
);
}
また、似たコード例として、 Viceroy の README の中の test runner の使い方の章でも様々な粒度の結合テストのコードの記述例が紹介されており、こちらもバリエーションの観点で参考になるかと思います。
#[test]
fn test_using_client_request() {
let client_req = fastly::Request::from_client();
assert_eq!(client_req.get_method(), Method::GET);
assert_eq!(client_req.get_path(), "/");
}
#[test]
fn test_using_bodies() {
let mut body1 = fastly::Body::new();
body1.write_str("hello, ");
let mut body2 = fastly::Body::new();
body2.write_str("Viceroy!");
body1.append(body2);
let appended_str = body1.into_string();
assert_eq!(appended_str, "hello, Viceroy!");
}
#[test]
fn test_a_handler_with_fastly_types() {
let req = fastly::Request::get("http://example.com/Viceroy");
let resp = some_handler(req).expect("request succeeds");
assert_eq!(resp.get_content_type(), Some(TEXT_PLAIN_UTF_8));
assert_eq!(resp.into_body_str(), "hello, /Viceroy!");
}
上記のような結合テストのほかに、rstest など任意のテスト用 crate を利用しながらになると思いますが適宜必要な箇所で単体テストについても準備した上で、標準の cargo test あるいは cargo-nextest 等のランナーを用いてテストを書いていくイメージになることが多そうです。
Go コード例
Go の結合テストのコード例は以下の GitHub リポジトリ中に豊富にサンプルがあります。
一部を抜粋して紹介すると、以下のような形で記載するイメージです。Go の標準の net/http/httptest パッケージにより提供される ResponseRecorder というテストダブル(スパイ)に似たスパイが Go SDK の github.com/fastly/compute-sdk-go/fsttest パッケージにより同名の ResponseRecorder という名前で提供されていますので、本稿で紹介している handler 関数のような対象に対して結合テストを書く際はこちらのスパイを使って効果的にテストを書くことも可能です。
func TestHelloWorld(t *testing.T) {
handler := func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
fmt.Fprintf(w, "Hello, TinyGo!")
}
r, err := fsthttp.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
w := fsttest.NewRecorder()
handler(context.Background(), w, r)
if got, want := w.Code, fsthttp.StatusOK; got != want {
t.Errorf("Code = %d, want %d", got, want)
}
if got, want := w.Body.String(), "Hello, TinyGo!"; got != want {
t.Errorf("Body = %q, want %q", got, want)
}
}
JavaScript のコード例
JavaScript に関してはプラクティスと言えるレベルでのまとまった知見が Fastly のブログやリポジトリ上で公開されている状況ではないのですが、今年コミュニティサイトで興味深い関連議論があったので本節では主にそのスレッドについて紹介します。
テストコードを書いていく際、Config Store などの fastly:...
で提供されるパッケージについて Viceroy などの外部プロセスを使わずにどのようにモックすべきか(できるか)という問いかけが発端でしたが、その中で質問された方がイメージされる現状取りうるモックの手段として、vitest を使う際に便利な ysugimoto さんの vite-plugin-fastly-js-compute を紹介されていました。
example フォルダ を見ると KVStore を import している(そして本プラグインによって名前解決がされている)サンプルを確認することができます。また、実際に質問者の方もこのプラグインを利用してモックを実現したとのことでサンプルコードについてもスレッドの中で紹介されています。vitest は一つの例なのでこのフレームワークに拘る必要はないのですが、モックの手法や実際の結合テストの例として参考になるかと思います。
Pro Tip: 結合テストにおけるモックのアイデア
以下のような設定を fastly.toml に行うことで、Viceroy(あるいは fastly compute serve)では Config Store や Secret Store, KV Store のモック動作をさせることが可能です。
[local_server]
[local_server.config_stores]
[local_server.config_stores.examplestore]
format = "inline-toml"
[local_server.config_stores.examplestore.contents]
readme = "dummy.txt"
このような設定をした上で Viceroy を起動すると、examplestore
というストア名の ConfigStore のモック実装として機能してくれるため、例えば以下のようなコードがローカルでも動作するようになります。
let store = ConfigStore::open("examplestore");
store.get("contents")
E2E テスト
世の中には数多くの E2E テストフレームワークがあると思いますし、テストの書き方も結合テストのそれに比べて難しいことはそこまで多くないのではないかと思います(通常の Web サーバに対する E2E テストを書いていく工程と何ら変わりはないかと思います)。
それぞれの環境やプロジェクトの状況に適したフレームワークを選択するのが一般的に良いかと思いますが、Fastly Compute ではどのようなフレームワークにおいても使いやすい、ローカルの Viceroy 起動に特化した小さなライブラリを用意しています。
利用例としては以下のようなコードとなります。特徴としては fastly CLI コマンドのインストールを前提とした小さなライブラリになっていて、起動時のコマンド指定やコマンドが実行される root フォルダの指定、それから Viceroy が起動するローカルのアドレスやポートなどについて指定ができます。
const app = new ComputeApplication();
await app.start({
// Set 'startCommand' to the command that starts your server.
startCommand: 'npm run start',
// Set 'appRoot' to the directory in which to run 'startCommand'.
appRoot: '/path/to/approot',
});
より詳細な使い方については js-compute-testing ライブラリの README ページで確認することができます。E2E テストの実行環境について、どんなフレームワークにおいてもちょい足しで使えるので、ローカルで Viceroy (実態としては fastly compute serve コマンド)を走らせながら E2E テストを実行するような場合には利用を検討してみても良いかもしれません。
まとめ
本稿では Compute と DevOps(CI/CD) という切り口のシリーズ第一回目として、Rust/Go/JS 各 SDK を使った結合テストや E2E テストの書き方やプラクティスを紹介しました。Compute では 3 つの言語が公式で提供されている関係で、テストについてもそれぞれの言語や SDK で異なった手法が取られることが多くまとまった知見になりにくい部分があるのですが、それでも本稿を執筆している 2024 年末現在で色々な知見が公開されてきていましたので、現時点での最新のプラクティスという形で概要をまとめてみました。
次回は Compute と DevOps(CI/CD) という切り口のシリーズ第二回目として、Terraform を使った構成管理について概要を紹介します。
Discussion