CI における Fastly 設定のテスト

自動テストは、現代のすべての Web アプリケーションにおいて重要な役割を果たしています。お客様は稼働しているサービスに VCL 設定をプッシュする前にテストする機能を必要としています。API を使用してサービスをセットアップし、自動化ツールで VCL をプッシュし、テストリクエストを実行してその結果を調べることは以前から可能でした。しかし、これにはいくつか不便なところがありました。

  • ゼロからサービスをセットアップするには、相当な数の API コールが必要です。

  • サービスのセットアップと設定の同期には数秒しかかかりませんが、デプロイしたことを伝えるフィードバックメカニズムはありません。設定のデプロイとその後のテストには迅速なターンアラウンドタイムが必要であるため、これは不便です。

  • Fastly の内部メカニズムをテストすることまではできません。アサートできるのは外部から見える設定の影響だけです。これは、本質的には「ブラックボックス」テストになります。

  • その後、テストサービスを削除しないと、インターネットに晒されたサイトにオープンなセキュリティ脆弱性を残したままになってしまう可能性があります。

最近、Fastly Fiddle ツールユニットテストを追加する方法について書きました。これにより、簡潔なテストシンタックスとアサート可能な豊富な計測データが使用できるようになります。

Fiddle での�テスト実行

もちろん、Mocha のような使い慣れたツールでこの機能を使用して CI テストを実行できるのが理想的です。

$ npm test
> mocha utils/ci-example.js
Service: www.example.com
Request normalisation
GET /?bb=2&ee=e2&ee=e1&&&dd=4&aa=1&cc=3 HTTP/2
✓ clientFetch.status is 200
✓ events.where(fnName=recv).count() is 1
✓ events.where(fnName=recv)[0].url is "/?aa=1&bb=2&cc=3&dd=4&ee=e1&ee=e2"
Robots.txt synthetic
GET /robots.txt HTTP/2
✓ clientFetch.status is 200
✓ clientFetch.resp includes "content-length: 30"
✓ clientFetch.bodyPreview includes "BadBot"
✓ originFetches.count() is 0
GET /ROBOTS.txt HTTP/2
✓ clientFetch.status is 404
10 passing (37s)

Fastly Fiddle は、Fastly を通過するリクエストの結果を詳細な JSON 形式のデータ構造として表す API を公開しています。それを受け取り、Chai などのアサーションライブラリを使用してアサーションを実行することができます 。しかし、私たちは Fiddle のファーストクラスの機能としてテストのサポートを追加したかったのです。

組み込みのテストサポートの導入により、前回取り上げた簡潔で表現力豊かなテストシンタックスを使用して、テストケースを fiddle 自体に直接ベイクすることができます。Fiddle HTTP API と fiddle を表現するために使用するデータ構造を理解すれば、自動化してこのテストエンジンをリモートで動かすことができます。これを可能にするために、Mocha抽象レイヤーを作成しました。まず、それを使用する方法を見てみましょう。その後、動作の仕組みを説明します。始める前に、Fastly Fiddle は Labs プログラムの一環として提供されている実験的なサービスであることに注意してください。このサービスをコードデプロイの重要な依存関係にしないでください。また、個々の fiddle は認証なしで簡単にアクセスできるように設計されていることにも注意してください。fiddle の URL を所有している人にコードが公開される可能性があります。

独自の CI テストの実行

まず、GitHub からコードを取得し、依存関係をインストールします。

$ git clone git@github.com:fastly/demo-fiddle-ci.git fastly-ci-testing
$ cd fastly-ci-testing
$ npm install

この最後のステップは、システムに NodeJS (10+) がインストールされていることを前提としています。準備ができたら、npm test を実行してサンプルテストを実行し、この記事の上部にあるような出力を確認することができます (テストには1分ほどかかります)。

順調に進んでいますか ? ここからは、面白い部分です。独自のサービス設定を定義し、独自のテストを作成しましょう。src/test.js を開き、一番上の部分を見てください。

testService('Service: example.com', {
spec: {
origins: ["https://httpbin.org"],
vcl: {
recv: `set req.url = querystring.sort(req.url);\nif (req.url.path == "/robots.txt") {\nerror 601;\n}`,
error: `if (obj.status == 601) {\nset obj.status = 200;\nset obj.response = "OK";synthetic "User-agent: BadBot" LF "Disallow: /";\nreturn(deliver);\n}`
}
},
scenarios: [
...
]
});

この最初の部分では、spec プロパティ内で、エッジにプッシュするテスト用のサービス設定を定義します。この時点で、テストするサービス設定で fiddle を設定しておくと便利です。その場合は、fiddle UI のメニューをクリックし、'EMBED' を選択すると、JSON エンコードされたバージョンの fiddle が表示されます。vcl プロパティと origins プロパティをテストファイルの spec セクションにコピーします。または、この記事の後半でデータモデルを確認することもできます。

これで、いくつかのテストシナリオを設定できるようになりました。各シナリオでは、設定の機能を1つテストします。例えば、位置情報の取得、画像の最適化、合成応答の生成、A/B テストなどを行っているサービスがあるとします。各機能をテストするには、異なるパターンのリクエストを送信する必要があります。

シナリオの一例を見てみましょう。

testService('Service: example.com', {
spec: { ... },
scenarios: [
{
name: "Caching",
requests: [
{
path: "/html",
tests: [
'originFetches.count() is 1',
'events.where(fnName=fetch)[0].ttl isAtLeast 3600',
'clientFetch.status is 200'
]
}, {
path: "/html",
tests: [
'originFetches.count() is 0',
'events.where(fnName=hit).count() is 1',
'clientFetch.status is 200'
]
}
]
},
...
]
});

このシナリオは「キャッシング」と呼ばれるもので、リクエストが2回繰り返された場合、2回目は Fastly がキャッシュ内に保持することをテストしています。繰り返しますが、すでに fiddle 内に、必要とする設定がある場合は、fiddle メニューの 'EMBED' オプションを使用してリクエストデータをエクスポートできます (ここでは requests プロパティが必要です)。それ以外の場合は、この記事の後半で、完全なデータモデルを確認できます (前回の記事で説明したテストシンタックスを除く)。

src/test.js ファイルを希望どおりにカスタマイズしたら、npm test で実行できます。このコマンドは、package.json ファイルからテストスクリプトをトリガーします。これは、プロジェクトのルートから実行するのと同じことです。

$ ./node_modules/.bin/mocha src/test.js --exit

ノードだけでなく、mocha 実行可能ファイルを使用してスクリプトを実行していることに注意してください。これは重要なので、npm test をショートカットとして使用することをおすすめします。また、一部の CI ツールはスマートで、package.json で宣言されたテストスクリプトを検出し、それを使用してテストを開始します。

うまくいっていれば、Mocha が今、テストを実行しています。テストが実行されたら、選択した CI ツールでこれを実行するようにセットアップし、パスしたら新しい設定を Fastly アカウントにデプロイするパイプラインを作成します。

それではその仕組みをご説明します。

Fastly Fiddle クライアント

私は、NodeJS の Fastly Fiddle ツール用に不完全ですが非常に小さいクライアントを作りました。これは、src/fiddle-client.js としてプロジェクトに含まれています。これは、fiddle をパブリッシュ、クローン、実行するためのメソッドを公開します。

  • get(fiddleID) : fiddle を返します。

  • publish(fiddleData) : fiddle を作成または更新し、正規化された fiddle データを返します。

  • clone(fiddleID) : fiddle を新しい ID にクローンし、正規化された fiddle データを返します。

  • execute(fiddleOrID, options) : Fiddle を実行し、結果データを返します。

fiddle に対する操作を実行するには、fiddle データモデルと結果データモデルを理解する必要があります。

fiddle データモデル

fiddle 型のオブジェクトは次のようになります (UI でのみ使用できる cosmetic プロパティは省略)。















































































vclオブジェクトサブルーチンのためにコンパイルする必要のあるソースコードに対する VCL サブルーチン名のマップ。
  .init文字列任意の VCL サブルーチンの外にある、初期化スペースの VCL コード。 このスペースを使用して、カスタムサブルーチン、ディクショナリ、ACL、ディレクターを定義します。
  .recv文字列recv サブルーチン用の VCL コード
  .hash文字列ハッシュサブルーチン用の VCL コード
  .hit文字列ヒットサブルーチン用の VCL コード
  .miss文字列ミスサブルーチン用の VCL コード
  .pass文字列パスサブルーチン用の VCL コード
  .fetch文字列フェッチサブルーチン用の VCL コード
  .deliver文字列配信サブルーチン用の VCL コード
  .error文字列エラーサブルーチン用の VCL コード
  .log文字列ログサブルーチン用の VCL コード
オリジン配列VCL公開するオリジンのリスト
  [n]文字列ホスト名やスキームとしてのオリジン。「https://example.com」。VCL F_origin_{arrayIndex}}ように公開。
リクエスト配列fiddle実行されたときに実行するリクエストのリスト
  [n]オブジェクト1つのリクエストを表すオブジェクト
    .method文字列リクエストに使用する HTTP メソッド
    .path文字列リクエストへの URL パス (必ず / で始まります)
    .headers文字列リクエストとともに送信するカスタムヘッダー
    .body文字列リクエストボディのペイロード (これを設定すると、Content-Length ヘッダーがリクエストに自動的に追加されます)
    .enable_clusterブールPOP クラスタリングを有効にするかどうか。 設定を有効にすると、キャッシュキーの計算後、実行はそのキーの標準的な保存場所であるストレージノードに転送されます。 無効にすると、プロセスは最初にリクエストを受信したランダムエッジノードで完全に実行されます。Fastly POP多数の個別のサーバーで構成されているため、同じオブジェクトに対する連続したリクエストがキャッシュヒットにならない可能性があります。
    .enable_shieldブール設定を有効にすると、リクエストを処理する POP指定されたオリジンシールドではない場合、バックエンドの代わりにオリジンシールドが使用されるため、キャッシュミスは強制的にオリジンに送信される前に2つの Fastly POP通過します。
    .enable_wafブールFastly Web アプリケーションファイアウォールを介してリクエストをフィルタリングするかどうか。設定を有効にすると、追加の waf イベントが結果データに含まれ、WAFルールに致した場合、リクエストがエラーをトリガーする可能性があります。  Fiddle使用される WAF は、最小限のルールセットで設定されており、独自のサービスの WAF 設定の適切なシミュレーションではありません。
    .conn_type文字列http (安全ではない)、h1、h2、または h3いずれか。
    .source_ip文字列ジオロケーションなどのエッジクラウド機能において、リクエストの発信元とみなされる IP。
    .follow_redirectsブールレスポンスがリダイレクトで、有効な Location ヘッダーが含まれている場合に自動追加リクエストを挿入するかどうか
    .tests文字列/
配列
前回の投稿でご紹介したテストシンタックスに従い、改行処理で区切られたテスト式のリスト。  これはテキスト blob フィールドですが、必要に応じてテスト式を配列として送信することもできます。

これで fiddle の作成、更新、実行ができるようになりました。execute() を除くすべてのメソッドは、fiddle オブジェクトにデータを約束します (つまり、上記のモデルにデータを適合させます)。execute メソッドは、結果データオブジェクトにデータを約束します。

結果データモデル

execute() メソッドによって約束されたデータは次のようになります。clientFetches[...].tests プロパティに注意してください。このプロパティについては、ここで詳しく説明します。




























































































































id文字列実行セッションの識別子
requestCount数字Fastly対して行われたクライアント側のリクエストの。  clientFetches オブジェクトのアイテム数と同じになります。
clientFetchesオブジェクトクライアントアプリケーションから Fastly サービスへのリクエストのマップ。キーは、イベントリスト内のオブジェクトの reqID プロパティに対応する、ランダムに生成された1回限りの識別子です。
  .${reqID}オブジェクト一のクライアントリクエスト。
    .req文字列リクエストステートメントを含むリクエストヘッダー
    .resp文字列レスポンスのステータス行を含むレスポンスヘッダー
    .respType文字列解析された Content-type レスポンスヘッダーの (MIME タイプのみ)
    .isTextブールレスポンスボディをテキストとして扱うことができるかどうか
    .isImageブールレスポンスボディを画像として扱うことができるかどうか
    .status数字HTTP レスポンスのステータス
    .bodyPreview文字列ボディを UTF-8 テキストでプレビュー 1,000字で切り捨て)
    .bodyBytesReceived数字受信したデータ
    .bodyChunkCount数字受信したチャンクの
    .completeブールレスポンスが完了したかどうか
    .trailers文字列HTTP レスポンスのトレーラー
    .tests配列このクライアントリクエストに対して実行されたテストのリスト
      [idx]オブジェクトテスト結果はオブジェクトです
        .testExpr文字列以前に学んだテストシンタックス基づいたテスト
        .passブールテストが成功したかどうか
        .expected任意期待された (文字列表現も testExpr含まれています)
        .actual任意観測された
        .detail文字列アサーション失敗の説明
        .tags配列テストパーサーによってテストに適用されるフラグ。現在、async slow-async含まれています。
    .testsPassedブールこのリクエストの tests プロパティで定義されたすべてのテストが成功したかどうかを記録する便利なショートカット。
originFetchesオブジェクトFastly サービスからオリジンへのリクエストのマップ。キーは、イベントリスト内のオブジェクトの vclFlowKey プロパティに対応するランダムに生成された1回限りの識別子です。
  .${vclFlowKey}オブジェクト一のオリジンリクエスト
    .req文字列リクエストメソッドパス、HTTP バージョン、ヘッダーのキー/値のペア、リクエストボディを含む HTTP リクエストのブロック
    .resp文字列レスポンスのステータス行とレスポンスヘッダー (ボディ以外)含む HTTP レスポンスヘッダー
    .remoteAddr文字列オリジンの解決済み IP アドレス
    .remotePort数字オリジンサーバーのポート
    .remoteHost文字列オリジンサーバーのホスト
    .elapsedTime数字オリジンフェッチに要した合計時間 (ミリ秒)
イベント配列実行セッションの処理中に発生したイベントの時系列のリスト。
  [idx]オブジェクト各配列要素は1つの VCL イベントです
    .vclFlowKey文字列イベントが発生した VCL フローの ID。  VCL フローは最も内側の相関識別子です。  ステートマシンを介する各片道トリップは1つの VCL フローであるため、vclFlowKey ごとに、 fnNameつき最大1つのイベントが発生します。  VCL フローはオリジンフェッチをトリガーする可能性があるため、この VCL フローにオリジンフェッチが関連付けられている場合、この識別子を使用して originFetches コレクションに記録されます。
    .reqID文字列このイベントをトリガーしたクライアントリクエストの ID。  クライアントリクエストは、リスタート、ESI、セグメント化されたキャッシュ、またはオリジンシールドが原因で、複数の VCL フローをトリガーする場合があります。  クライアントリクエストの詳細は、この識別子を使用して clientFetches コレクションに記録されます。
    .fnName文字列イベントの種類。recv、hash、hit、miss、pass、waf、fetch、deliver、error、log など。
    .datacenter文字列このイベントが発生した Fastly データセンターの所在地を識別する3文字のコード (LCY、JFK、SYD など)
    .nodeID文字列このイベントが発生したサーバーの識別番号
    .<various>イベントの fiddle UI報告されたプロパティはすべて、イベントの結果データに含まれます。
インサイト配列実行中に検出および報告された警告とベストプラクティス違反のリスト。

これをテストハーネスに接続するにはどうすればよいでしょうか。世界で最も人気のあるテスト実行プログラムの1つである Mocha を使用します。

Mocha テストケースジェネレーター

Mocha は、テスト仕様ファイルを記述するためにごく一般的なメカニズムを使用しています。CLI ツールはスクリプトファイルを呼び出すために使用され、CLI は自動的に特定の関数 (テストスイートを作成する describe 関数やbeforebeforeEachafterafterEachのようなライフサイクルフックと共に個々のテストを指定するための関数) が存在する環境を作成します。

describe('My application', function()) {
let db;
before(async () => {
db = setupDatabase(); // Imaginary function
});
it("should load a widget", async function() {
const widget = db.get('test-widget');
expect(widget).to.have.key('id');
});
});

この方法の問題は、私たちのテストケースではコードではなくデータとして表現する必要があるため、上記の方法で宣言できないことです。幸いなことに、Mocha では、グローバルな Mocha が明示的にインポートされる場合は、従来のコンストラクターを介してテストを作成することもできます。

const Mocha = require('mocha');
const test = new Mocha.Test('should load a widget', function () { ... });

describe() コールバック内で、これが Mocha Suite オブジェクトのインスタンスになります (このため、コールバックにアロー関数を使わないことも重要です)。コールバックが最初に実行されたときに、少なくとも1つのテストを同期させて作成する場合、Mocha により before() コードが実行されます。このコールバックでは、ハードコードされたテストをキャンセルし、動的なテストを作成することができます。

const Mocha = require('mocha');
describe('My application', function()) {
it ("has one hard-coded test", function () { return true });
before(async () => {
this.tests = []; // Delete the hard-coded test
this.addTest(new Mocha.Test('satisfies this dynamic test', function () { ... });
});
});

ここで、テスト仕様のデータから動的テストに入力する必要があります。src/test.js ファイルは、(name, data) のシグネチャを持つと期待される関数をインポートします。そのため、src/fiddle-mocha.jsのスキャフォールドを開始するには、これをエクスポートします。

module.exports = function (name, data) {
// Create a testing scope for each Fastly service under test
describe(name, function () {
this.timeout(60000);
let fiddle;
// Push the VCL for this service and get a fiddle ID
// This will sync the VCL to the edge network, which takes 10-20 seconds
before(async () => {
fiddle = await FiddleClient.publish(data.spec);
await FiddleClient.execute(fiddle); // Make sure the VCL is pushed to the edge
});
...
})
};

エクスポートされた関数が呼び出されるたびに、テスト対象のサービス設定を表すテストスイートが作成されます。デフォルトでは Mochaの タイムアウトは2秒となっていますが、これではリモートテストを行うには十分ではないので、これを増やしています。

サービス設定は、このサービスレベルスイートの before() コールバックで fiddle にパブリッシュすることができます。また、publish が検証するのは設定が有効であるかどうかだけであり、ネットワーク全体に正常にデプロイされたことを確認するわけではないので、実行をトリガーする価値はあります。execute() がそれを行います。

これで、各テストシナリオのサブスイートを作成することができるようになりました。

module.exports = function (name, data) {
describe(name, function () {
...
for (const s of data.scenarios) {
describe(s.name, function() {
it('has some tests', () => true); // Sacrificial test
before(async () => {
this.tests = []; // Remove the sacrificial test from the outer suite
const result = await FiddleClient.execute({
...fiddle,
requests: s.requests
}, { waitFor: 'tests' });
for (const req of Object.values(result.clientFetches)) {
if (!req.tests) throw new Error('No test results provided');
const suite = new Mocha.Suite(req.req.split('\n')[0]);
for (const t of req.tests) {
suite.addTest(new Mocha.Test(t.testExpr, function() {
if (!t.pass) {
const e = new AssertionError(t.detail , {
actual: t.actual, expected: t.expected, showDiff: true
});
throw e;
}
}));
}
this.addSuite(suite);
}
});
});
}
});
};

それでは、これをステップスルーしていきます。

  1. それぞれのシナリオについて、describe() を呼び出します。これにはシナリオ名を渡し、コールバックを設定しています。コールバックは、すぐに Mocha に呼び出されます。

  2. before() コールバックが実行されるように仮のテストを定義します。

  3. before() コールバックでは、仮のテストを削除し、fiddle を実行し、このシナリオ用に定義されているリクエストでパッチを適用します。waitFor: 'tests' は、テスト結果が利用可能になったときに API クライアントに約束の解決のみを行うように指示します。

  4. シナリオを構成する各クライアントリクエストを繰り返し処理し、リクエストのテストスイートを作成します。スイートの名前としてリクエストヘッダーの最初 (例 : "GET / HTTP/2") を使用しています。

  5. そのリクエストで実行されたテストごとに、新しい Mocha テストを作成し、テストスイートに追加します。失敗した場合は、テスト結果の関連プロパティーを AssertionError に渡します。AssertionError は、ネイティブの JavaScript エラーオブジェクトの拡張であり、expectedactualshowDiff の追加プロパティを定義します。Mocha はこれらのプロパティを知っているので、テストレポートでより良いユーザーエクスペリエンスを提供できます。

  6. リクエストスイート (suite) をシナリオスイート (this) に追加します。

  7. シナリオスイートは、サービスレベルスイートの内部で、describe コールを介して (ルートスイートとして) Mocha ランナーにすでに登録されています。

Mocha は、(test.jstestService を呼び出すことで) 1つ以上のルートスイートを発見します。各ルートスイートには1つ以上のシナリオスイートが含まれ、各シナリオスイートには1つ以上のリクエストスイートが含まれます。リクエストスイートには、リクエストで定義されたテストが含まれます。

最後に

CI でのテストは、Fastly サービスの新しいバージョンを有効にしたときの思わぬトラブルを避けるための優れた方法であり、Fiddle はそれを支援する便利なツールです。マージする前にプルリクエストをテストする CI プロセスが既にある場合は、Fastly サービスのテストを統合することも検討してください。この記事の内容があなたの環境でうまくいったかどうか大変興味があります。よろしければ、経験したことをコメントやメールでお聞かせください。

ただし、このサービスをコードデプロイの重要な依存関係にしないでください。これは、Fastly Labs プログラムの一環として提供される試験的なサービスであり、SLAの対象ではありません。フィードバックを収集するために提供しています。

Andrew Betts
Principal Developer Advocate
投稿日

この記事は9分で読めます

興味がおありですか?
エキスパートへのお問い合わせ
この投稿を共有する
Andrew Betts
Principal Developer Advocate

Andrew Betts は、Fastly の Principal Developer Advocate として、世界各地の開発者と協力し、Web の高速化、セキュリティ強化、信頼性と使いやすさの向上に努めています。Fastly 入社前は、Web コンサルティング会社 (後に Financial Times により買収) を設立し、Financial Times の先駆的な HTML5 ベースの Web アプリケーションの開発を統括したほか、同紙のラボ部門の設立にも携わりました。また、W3C Technical Architecture Group (World Wide Web の開発を導く9名で構成される委員会) の選出メンバーでもあります。

Fastly試してみませんか ?

アカウントを作成してすぐにご利用いただけます。また、いつでもお気軽にお問い合わせください。