Nordlet

← ブログ

会計 API の本番稼働前テスト計画

エンジニアリングチームのための信頼性プレイブック。二重書き込み、冪等キーの再利用、Webhook 署名、順序不同のイベント、十進数金額、スコープ、期末ロック、およびリカバリを解説します。

Nordlet Team · · 2 分で読める

会計 API からの 201 Created レスポンスは、ほとんど何も保証してくれません。それはサーバーが応答したことを確認するだけであり、1 件の請求書のみが存在すること、借方と貸方が一致していること、伝票番号が一度だけ採番されたこと、あるいは接続断後の再試行によって 2 つ目のコピーが計上されないことは保証しません。本番環境における会計データの重複の大部分は、このギャップから発生します。つまり、サーバー側でミューテーションがコミットされた一方で、クライアントは不明瞭な結果を抱えたまま、新しいキーを使って静かに再試行してしまうケースです。

この計画は、会計 API を実際の帳簿に組み込む前に評価を行うエンジニアリングチームのためのものです。ハッピーパスではなく、障害時の振る舞いに焦点を当てています。以下の各セクションでは、何をテストし、何をアサートし、何を合格とみなすかを説明します。ここでの実装コンテキストは Nordlet であり、その API はスコープ付きベアラキー、十進数文字列の金額、冪等キー、および署名付き Webhook を使用します。正確なエンドポイントの構文とレスポンスの契約については、推測に頼るのではなく、API 規約およびはじめにのページを参照してください。

「合格」の本当の意味

会計連携の安全性を証明できるのは、ステータスコードではなくセマンティクスを検証した場合のみです。すべてのシナリオにおいて、テストスイートは以下を確認する必要があります。

  • ビジネス上の副作用: 顧客、請求書、仕訳、番号採番、または支払いの遷移が正確に 1 つだけ存在すること。
  • 会計上の整合性: 借方が貸方と一致し、金額の精度がシリアライゼーション後も保持されること。
  • リプレイ時の振る舞い: 再試行によって新しいレコードが作成されるのではなく、元の結果が返されること。
  • 耐久性: 操作のコミット時に監査記録と Webhook イベントが存在すること。
  • 副作用のない失敗: 拒否されたリクエストは伝票番号を消費せず、部分的な元帳行を書き込まないこと。
  • リカバリ: 一時的なエラーは、重複や永続的なデータ損失なしに再試行できること。

Google の API ガイドラインでは、明確に線引きがされています。繰り返し実行しても最終状態が変わらない操作のみが再試行安全 (idempotent) であり、API はあらゆる失敗を盲目的に再試行するのではなく、どの操作が冪等であるかを特定すべきであるとしています。

使い捨てで観測可能な環境の構築

本番の帳簿ではなく、サンドボックス企業を使用してください。Nordlet のサンドボックス企業は実際の企業と同じモジュールおよび API の振る舞いを使用し、テストデータとしてマークされるため、大量の重複テストや障害テストでも安全に使用できます。このフラグは作成後に変更できないため、後から実際の帳簿をテストデータに変更することはできません。サンドボックスの使用量は実際の企業の利用と同様に計測および課金されるため、料金ページを参照して負荷テストの規模を計画してください。

テストを記述する前に、以下を準備します。

  • isSandbox: true に設定された専用のサンドボックス企業。
  • テスト対象のモジュールのみにスコープが制限されたテスト API キー。
  • 生のリクエストボディを保持する Webhook エンドポイントまたはローカルのトンネル。
  • シードされた顧客、サプライヤー、および既知の勘定科目体系。
  • 最低 2 つの会計期間 (1 つはオープン、もう 1 つはロック済み)。
  • 実装がサポートしている場合は、テストクロックまたは日付フィクスチャ。
  • すべてのリクエスト試行、冪等キー、レスポンスハッシュ、Webhook 配信、および消込結果を記録するローカルストア。

共有のステージングデータに対して重複テストを実行しないでください。前の実行による浮いた顧客や請求書が存在すると、後続のアサーションが曖昧になります。

すべてのミューテーションのフィクスチャには、メソッド、エンドポイント、完全なボディ、冪等キー、キーのスコープ、クライアント相関 ID、開始および終了のタイムスタンプ、レスポンスのステータス、ヘッダー、ボディ、リクエスト ID、およびレスポンス到着前に接続が切断されたかどうかの情報をキャプチャする必要があります。金額には "75.0000" などの固定の十進数文字列を使用してください。Nordlet では、金額を最大 4 桁の小数を持つ十進数文字列として表現し、サンプル請求書でもその形式を使用しています。

設定のもう半分は障害注入です。テストハーネスは、レスポンスの遅延、サーバーがリクエストを受信した後の TCP 接続の切断、合成された 429/500/502/503/504 レスポンスの返却、Webhook 受信側からの非 2xx の返却、同一 Webhook の二重配信、イベントの順序入替、Webhook ボディの 1 バイトの破損、キーを維持したままの冪等キーペイボディの変更、2 つの同一リクエストの並行実行、および設定と計上の間の期間ロックを行うことができなければなりません。これらのシナリオは、100 個のハッピーパスの例よりも価値があります。

リトライロジックを記述する前に契約を固定する

OpenAPI スキーマを生成または取得し、クライアントと比較します。Nordlet はAPI リファレンスで OpenAPI 仕様を公開し、その同じ仕様から型付き SDK を生成するため、手書きの推測よりもスキーマ駆動のテストの方が優れています。

必須フィールドと型、十進数文字列のフィールド、日付とタイムスタンプの形式、許可されるアクションとスコープ、エラーエンベロープの形状、Webhook イベントスキーマ、冪等キーを受け入れるミューテーション、および作成されたオブジェクトを識別するレスポンスフィールドを検証します。

合格基準: スキーマが許可している範囲で、クライアントが無効なペイボディをローカルで拒否すること。サーバーのレスポンスがドキュメント化されたスキーマと一致すること。無効な十進数、不正な日付、および欠落している必須フィールドが構造化されたバリデーションエラーを生成すること。決定論的なバリデーション失敗に対しては再試行が行われないこと。

キャッチすべき障害モード: クライアントが "121.0000" をバイナリの浮動小数点数としてパースし、後でわずかに異なる金額をシリアライズしてしまうこと。整数は通過しますが、消費税行や端数数量は失敗します。金額はエンドツーエンドで文字列としてテストしてください。

認証とテナントの分離

読み取り用と書き込み用で別々のキーを使用します。その後、キーなし、無効なキー、必要なモジュールスコープを持たない有効なキー、正しいスコープを持つ有効なキー、別の企業に属するキー、および異なる企業のオブジェクトを読み取ろうとする試しのグリッド全体をテストします。

アサーション:

  • 資格情報がないか無効な場合、401 が返されること。
  • 必要な権限を持たない有効なキーの場合、403 が返されること。
  • いずれの失敗においても会計上の副作用が作成されないこと。
  • 成功したリクエストであっても、別の企業のレコードを読み取ったり変更したりできないこと。
  • セキュリティ上の失敗は自動的に再試行されないこと。

合格基準: ステータスコードだけでなく、テスト後の読み取りによって認証の正確性とテナントの分離の両方を証明できること。

クリーンなトランザクションの証明

小さく決定論的なフローを実行します。テスト顧客を作成または検索し、十進数文字列の金額を持つ請求書を作成して発行し、請求書とその会計効果、監査記録を読み取り、Webhook を受信します。Nordlet の「はじめての方へ」のサンプルは、請求書の発行によってギャップのない番号が採番され、期間がオープンであることが確認され、バランスの取れた仕訳が同じトランザクション内で計上される様子を示しています。

請求書が期待される状態に到達すること、伝票番号が正確に 1 回だけ採番されること、借方が貸方と一致すること、消費税と行の合計が期待される四捨五入(half-up)の計算と一致すること、監査記録にアクターと変更内容が記載されること、Webhook が最終的に配信されること、および API レスポンスと後続の読み取りで請求書の ID と状態が一致していることをアサートします。

保持すべき重要な区別として、Webhook の到着はミューテーションがコミットされたことの証明ではありません。API レスポンスと再読み取りが、トランザクションに関する主要なアサーションとなります。Webhook は下流への通知を証明するものであり、個別にテストされます。

冪等性: 同じキー、同じボディ

ネットワークの試行ごとではなく、論理的なビジネス操作ごとに 1 つのキーを生成します。キー K とボディ B でミューテーションを送信し、作成された ID を保存してから、全く同じリクエストを再度送信します。

2 番目のレスポンスが同じ操作を表していること、オブジェクト ID が同一であること、そしてビジネスオブジェクト、仕訳、伝票番号、財務的影響がそれぞれ 1 つずつ存在することをアサートします。再プレイされたレスポンスには、ドキュメント化されたリプレイヘッダー(Nordlet の場合は x-idempotent-replay: true)が含まれている必要があり、監査証跡には 2 番目のミューテーションが表示されないはずです。

リトライループが期待すべき動作を変更する 2 つのストレージルールがあるため、直接テストしてください。保存された 4xx は他のレスポンスと同様にリプレイされますが、5xx はまったく保存されないため、サーバーエラー後の再試行ではリプレイではなく操作の再実行が行われます。また、キーには有効期限があり、Nordlet の場合は 24 時間後に期限切れとなるため、リプレイを利用できる期間が制限されます。

Nordlet のドキュメントでは、Idempotency-Key ヘッダーを追加することで、再試行されたリクエストが重複を作成するのを防ぐことが保証されており、その API 規約では正確なリプレイヘッダーとバイト単位のレスポンス要件が定義されています。この原則は、Stripe が公開しているモデルと一致しています。つまり、キーが論理的な操作を識別し、繰り返しに対しては保存された結果が返し、異なるパラメータでキーが再利用された場合は、暗黙的に変更されるのではなく拒否されるというものです。

合格基準: クライアントがリプレイウィンドウ内で同じリクエストを何回送信しても、正確に 1 つの財務状態遷移のみが存在すること。

冪等性: 同じキー、変更されたボディ

キー K とボディ B1 で元のリクエストを送信し、その後、金額、顧客、日付、または行のいずれかが異なるキー K とボディ B2 で 2 番目のリクエストを送信します。

期待される結果: リクエストがドキュメント化されたキー再利用エラー(Nordlet の場合は 422 idempotency_key_reuse)で拒否されること、元のレコードが変更されないこと、2 番目のレコード、番号、または監査ミューテーションが表示されないこと、そしてクライアントが新しいキーを生成して「修正」しないこと。B2 を暗黙的に受け入れてしまうと、リトライキーが安全でなくなってしまいます。

並行する重複リクエスト

同じキーを持つ 2 つの同一リクエストを同時に発火させます。ドキュメント化された振る舞いに応じて、一方の処理が実行されている間にもう一方は進行中競合を受け取る(Nordlet の場合は 409 idempotency_in_progress を返す)か、あるいは両方が最終的に同じ結果を観測します。

両方の呼び出しが独立した財務的影響を作成できないこと、高々 1 つの伝票番号しか採番されないこと、一時的な競合は新しいキーではなく同じキーで再試行されること、最終的なリソースが完全かつ読み取り可能であること、および冪等レコードが永久にスタックしたままにならないことをアサートします。

障害モード: 試行ごとに新しい UUID を発行するリトライライブラリは、1 つの論理的操作を複数の独立した書き込みに変えてしまい、冪等性を完全に台無しにします。

接続切断後の不確実な結果

これは、単一の最も重要な本番シミュレーションです。キー K でミューテーションを送信し、サーバーにそれをコミットさせ、レスポンスが到着する前に接続を切断し、結果を未知として記録してから、同じキー K で再試行し、リソースと監査データを読み取って消込を行います。

再試行によって元のレスポンスまたはドキュメント化されたリプレイレスポンスが返されること、財務的な副作用が正確に 1 つだけ存在すること、クライアントは成功したリプレイまたは消込の読み取りが行われた後にのみ操作を完了としてマークすること、そして新しいキーが生成されないことをアサートします。タイムアウトは「何も起きなかった」ことを意味するわけではありません。Stripe は、クライアントがレスポンスを受信する前にサーバーがミューテーションを処理している可能性がある接続エラーに対する保護として、冪等性を正確に説明しています。

保守的なリトライポリシー

リトライの決定は、HTTP メソッド単体ではなく、エラーの種類と操作が冪等であるかどうかの両方に依存します。

通常は再試行可能、同じキーを維持: 冪等なミューテーション後の接続リセットまたはタイムアウト、408、retry-after を尊重した後の 429、および 502/503/504。進行中の冪等性を示すレスポンスがドキュメント化されている場合、短い遅延の後に再試行できます。

消込なしでは再試行不可: 400/422 バリデーションエラー、401/403 認証失敗、見つからないリソースに対する 404、進行中のキーとは無関係なビジネス上の 409 競合、既存のキーにおける変更されたペイボディ、および期末ロックによる拒否。

ジッター付きのバウンド付き指数バックオフ、明示的な最大試行回数と経過時間、レート制限に対して尊重される retry-after、1 つの操作の再試行を通じて維持される同じキー、新しい操作ごとの新しいキー、および元の試行と再試行を分離するログをアサートします。

レート制限を単体でテストします。ドキュメント化されたしきい値(Nordlet ではデフォルトで API キーあたり毎分 300 リクエストが許可されており、一部の認証なしエンドポイントには独自のアドレスあたりの低い制限があります)を超えた後、サーバーがレート制限のレスポンスを返すこと、クライアントがエラーではなく遅延として retry-after をパースすること、叩き続けるのではなく一時停止すること、および制限されたミューテーションの再試行が元のキーを再利用することを確認します。x-ratelimit-* ヘッダーは残りの割り当て量を報告するため、事前アサーションとして適しています。フィクスチャが制限された試行とコミットされた試行を区別できない限り、レート制限テストを財務的アサーションから遠ざけておいてください。

Webhook 署名の検証

生のバイトを保持するレシーバーを構築します。各イベントについて、生ボディのキャプチャ、x-nordlet-signature ヘッダーの読み取り、シークレットを使用した期待される HMAC-SHA256 の計算、定数時間比較関数を使用した比較、検証後の JSON パースのみ、配信の記録、および耐久性のある受け入れ後の高速な 2xx の返却を行います。Nordlet は生のボディに対して HMAC 署名を行い、sha256=<hex> として送信し、指数バックオフで配信を再試行します。Stripe の Webhook ガイドラインでは、変更されていない生のボディの定数時間比較による検証が強調されています。

ネガティブテストを実行します。正しいボディとシークレット、間違えたシークレットを持つ正しいボディ、1 バイト変更されたボディ、空白を変更して再シリアライズされた JSON、欠落したヘッダー、不正な形式の署名、スキームにタイムスタンプが含まれている場合はリプレイされた古い配信、および間違ったサブスクリプションに配信された有効な署名。

合格基準: 正しい生のボディとシークレットのみが受け入れられること。無効な署名には非 2xx が返されること。無効なペイボディが会計キューに入らないこと。シークレットが通常のログに出力されないこと。イベントが耐久性をもって記録された後にのみハンドラーが応答を返すこと。

重複および順序不同の Webhook

配信の信頼性は API の冪等性とは別の問題であり、レシーバー側も冪等である必要があります。同じイベントを二重に配信する、sale_invoice.issued イベントの前に sale_invoice.paid イベントを配信する、永続化の後かつ応答の前に 500 を返す、消費者がすでに処理したイベントを再プレイするテストを行います。

二重配信によって支払い、ステータス変更、または仕訳アクションが重複しないこと、重複排除キーにプロバイダーの配信 ID またはイベント ID が存在する場合はそれを使用すること(任意のタイムスタンプではなく、ドキュメント化された複合キー)、順序不同のイベントが古い状態で新しい状態を上書きしないこと、順序だけでは不十分な場合に消費者が API から現在の状態を取得できること、および失敗した消費者の試行がキューまたはデッドレターパスを経由することなどをアサートします。Stripe は、配信順序が保証されないことを明示的に警告し、処理済みのイベント ID を保存することを推奨しています。

エンドポイントは軽量に保ちます。形状の検証、署名の検証、永続化またはエンキュー、応答、2xx の返却を行います。消込と通知は非同期処理で行うべきです。下流の処理を配信タイムアウトを過ぎるまでスリープさせることでテストします。正しいレシーバーであれば、耐久性のあるキューイングの後に依然として応答を返します。

オープンな会計期間とロックされた会計期間

Nordlet は、手動仕訳、伝票フロー、インポートの全体において、計上時に期間のロックを強制するため、複数の場所でこの制御をテストしてください。オープン期間、ロックされた期間、ロックの直前の境界、次のオープン期間の初日づけ、および API が両者を分離している場合は有効な伝票日だが無効な計上日の日付を持つ同等の文書を作成します。

ロックされた期間のケースについては、ドキュメント化された期間ロックエラー(Nordlet では 409 conflict)でリクエストが拒否されること、仕訳行が書き込まれないこと、伝票番号が消費されないこと、文書が計上状態に遷移しないこと、および成功の Webhook が発火しないことをアサートします。オープン期間のバージョンは、同じ財務値で成功するはずです。

競合テスト: オープン期間に対する計上を開始し、リクエストの途中で期間をロックし、コミットがロックをバイパスできないことを確認します。重要な合格基準は、コミットされた計上内容が、トランザクションがコミットされる前にロックされていた期間に決して着地しないことです。

監査可能性と消込

成功したすべてのミューテーションについて、ビジネスオブジェクトが存在すること、借方が貸方と一致する会計エントリが存在すること、監査記録が存在すること、Webhook イベントが存在するか取得可能であること、および操作記録がリトライも含めて同じ論理的操作を指すプロバイダーオブジェクト ID を指していることを確認します。

拒否されたすべてのミューテーションについて、部分的な文書が存在しないこと、契約で予約が許可されていない限り番号が消費されていないこと、元帳エントリが存在しないこと、成功の Webhook が存在しないこと、およびエラーに診断用のリクエスト ID が含まれていることを確認します。Nordlet の規約では、ミューテーションの監査可能性とバランスの取れた計上がプラットフォームの保証として説明されており、アプリケーションコード単体ではなく、コミット時の遅延データベーストリガーによってバランスが強制されます。HTTP レスポンスを信頼するのではなく、それらの保証をテスト後の消込クエリに変換してください。

コアテストマトリックス

領域 テストアクション 期待されるアサーション 合格基準
認証 キーなし、無効なキー、不十分なスコープ 構造化された 401/403、副作用なし 拒否されたすべての試行で会計レコードが作成されないこと
スキーマと金額 無効な十進数、日付、必須フィールド フィールドレベルのバリデーションエラー 再試行や部分的な書き込みが発生しないこと
通常の計上 オープン期間での請求書の作成と発行 1 つの文書、1 つのバランスの取れた仕訳、1 つの監査記録 再読み取りの状態と元帳が一致すること
同一キーのリプレイ 同一のミューテーションの繰り返し(同じキーとボディ) 元の結果がリプレイされる、重複なし オブジェクト ID、番号、仕訳効果、監査が単一であること
変更されたボディ、同じキー 異なる金額でキーを再利用 ドキュメント化されたキー再利用の拒否 元のレコードが変更されないこと、新しいものが何も書き込まれないこと
接続切断 コミット、切断、同じキーで再試行 リプレイされた結果 財務的副作用が正確に 1 つだけ存在すること
ロックされた期間 ロックされた期間の日付を持つ文書の計上 期間ロックによる拒否 仕訳、番号、成功 Webhook のいずれも存在しないこと
Webhook 署名 間違ったシークレット、変更されたボディ 受信側から非 2xx が返されること 正しい生のボディとシークレットのみが受け入れられること

私が最初に実行すること

時間が限られている場合は、最も高額なバグをキャッチする順序でテストを実行してください。単一のシナリオでほとんどの本番環境の重複を引き起こすため、まずは接続切断後の再試行から始めます。次に、同じキーと変更されたボディの冪等性のペアです。その次に期末ロックです。クローズされた月への計上は、後になって表面化する監査上の問題となるためです。署名検証と順序不同の Webhook がそれに続き、クライアントが安定した後は、完全なスキーマと認証のグリッドを CI で継続的に実行できます。

この計画の残りの部分は、これら 5 つをサポートするためのものです。サンドボックス企業に対してこれらをグリーンにし、消込の読み取りによって副作用を確認できれば、単一の本番元帳に触れる前に連携が安全であることを確認できます。はじめにから数分でサンドボックス企業とスコープ付きテストキーを作成し、機能リストに対して必要なモジュールを確認することができます。

FAQ

2xx レスポンスだけで会計書き込みの成功を確認するのに十分ですか?

いいえ。ステータスコードはサーバーが応答したことを確認するものであり、正確に 1 つの文書、1 つの番号採番、および 1 つのバランスの取れた仕訳が存在することを証明するものではありません。すべてのミューテーションテストにおいて、レスポンスの後にオブジェクト、その会計効果、および監査記録の再読み取りを続ける必要があります。最も危険なケースは逆方向、すなわちクライアントがレスポンスをまったく受け取らなかった一方で、操作がコミットされていたというケースです。

リクエストがタイムアウトしたとき、新しい冪等キーを生成すべきですか?

いいえ。これが本番環境での重複の大部分を引き起こす間違いです。キーはネットワークの試行ではなく、論理的なビジネス操作を識別します。同じキーで再試行し、その後リソースを読み取って消込を行います。試行ごとに新しい UUID を発行するリトライライブラリは、1 つの意図された請求書を複数の独立した書き込みに変えてしまいます。

異なるペイボディで冪等キーを再利用するとどうなりますか?

正しい API は、新しいボディを暗黙的に適用するのではなく、それを拒否します。Nordlet は 422 idempotency_key_reuse を返し、元のレコードを変更せずにそのまま残します。クライアント側では、これを新しいキーを生成して回避すべき条件としてではなく、修正すべきプログラミングエラーとして扱うべきです。

Webhook は、作成されたオブジェクトの再読み取りの代わりになりますか?

いいえ。Webhook の配信は下流への通知を証明するものであり、トランザクションが期待通りにコミットされたことを証明するものではなく、配信順序も保証されません。生のボディに対して署名を検証し、配信 ID またはイベント ID で重複排除を行い、イベントの順序だけでは何が変更されたかを判断できない場合はいつでも API から現在の状態を取得してください。

ロックされた会計期間への計上をテストするにはどうすればよいですか?

オープン期間とロックされた期間に同等の文書を作成し、ロックされたケースが仕訳行なし、消費された伝票番号なし、状態遷移なし、成功の Webhook なしで拒否されることをアサートします。その後、競合バージョンを実行します。オープン期間に対する計上を開始し、リクエストの途中で期間をロックします。コミットされた計上は、トランザクションがコミットされる前にロックされていた期間に決して着地してはなりません。

サンドボックス企業で大量の重複テストを実行できますか?

はい。サンドボックス企業は実際の企業のように振る舞い、テストデータとしてマークされ、保持期間後ではなく即座に削除されるため、破壊的テストや重複テストを行うのに最適な場所です。サンドボックスの使用量も実際の利用と同様に計測および課金されるため、無限にループさせるのではなく、ボリュームを計画してください。