Die Stripe-API in der CI mocken
Stripe gibt dir einen Testschlüssel, und der zeigt auf genau ein Konto. Dein Team benutzt ihn gemeinsam, und jeder CI-Job ebenfalls, auch die aus verschiedenen Branches, die gleichzeitig laufen.
Zwei davon starten zusammen. Der eine legt einen Kunden an, während der andere die Kunden auflistet und prüft, was zurückkommt. Der zweite Job scheitert damit an Daten, die nichts mit seinem Branch zu tun haben. Ein erneuter Lauf ist grün.
Die Lösung: nicht mehr teilen. Stripe veröffentlicht ein OpenAPI-Dokument, du betreibst ohnehin etwas, das so eines ausliefert, und der Rest ist eine Workflow-Datei.
Die naheliegende Variante, und wo sie endet
Die Spec in einen Service-Ordner legen, committen, pushen:
services/
stripe/
openapi.ymlDas ist das richtige Layout, und es wird deployt. Der Ordnername wird zum Service-Namen, aus Stripes /v1/charges wird also /stripe/v1/charges. An diesem Aufbau ist nichts falsch.
Das Problem ist die Spec. Stripe veröffentlicht 419 Pfade, die der Router zu 594 Routen ausbaut, in einer YAML-Datei von 6,1 MB. Auf meinem Laptop belegte der Prozess damit rund 881 MB Arbeitsspeicher, brauchte 805 ms für den Routen-Aufbau und beantwortete die erste Anfrage nach 756 ms.
Eine Simulation bekommt keinen Laptop. Sie bekommt die Speichergrenze, die zu deinem Tarif gehört, und im kostenlosen Tarif sind das 128 MB bei einer Sekunde Timeout. Die vollständige Stripe-Spec passt da nicht hinein, und das Scheitern ist deutlich.
Die Stripe-Spec auf die Endpunkte kürzen, die du aufrufst
Deine Testsuite fasst keine 419 Pfade an. Wahrscheinlich sind es fünf oder sechs: ein Kunde, ein Payment Intent, eine Rückerstattung und die zwei, drei Lesezugriffe drumherum. Alles andere ist Ballast, den du beim Parsen bezahlst.
Der Befehl simplify nimmt einen Filter entgegen und wendet ihn als Erstes an. Schreib also auf, was du wirklich benutzt:
# services/stripe/codegen.yml
filter:
include:
paths:
- /v1/customers
- '/v1/customers/{customer}'
- /v1/payment_intents
- '/v1/payment_intents/{intent}'
- '/v1/payment_intents/{intent}/confirm'
- /v1/refundsDann lass ihn direkt über Stripes veröffentlichtes Dokument laufen:
mockzilla simplify \
--config services/stripe/codegen.yml \
--output services/stripe/openapi.yml \
https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yamlCommitte das Ergebnis, oder lass die Zeile in der CI laufen und halte sie aus dem Repo heraus. Committen heißt: Was deployt wird, ist auch das, was du liest, und es veraltet, sobald Stripe etwas ausliefert. In der CI bauen heißt: Es veraltet nie, und eine Änderung des Anbieters erreicht deine Simulation, ohne dass jemand sie gesehen hat. Wichtig ist in beiden Fällen der Filter.
Das dauert unter einer Sekunde und macht aus 6,1 MB genau 1,19 MB. Sechs Pfade, zwölf Routen, ein Routen-Aufbau in 123 ms und die erste Antwort nach 161 ms. Dieselben Endpunkte und Schemas, dieselben generierten Formen, abzüglich vierhundert Pfaden, die in deiner Suite noch nie jemand aufgerufen hat.
Wenn das immer noch zu groß ist, sind als Nächstes die optionalen Felder dran. Stripes Schemas führen Hunderte davon, und die meisten deiner Assertions lesen drei oder vier Felder:
mockzilla simplify --config services/stripe/codegen.yml --optional 5 \
--output services/stripe/openapi.yml <url>Das landet bei 0,91 MB und behält fünf optionale Felder pro Schema. Wirfst du sie mit --optional 0 ganz weg, sind es 0,43 MB, ein Vierzehntel des Ausgangswerts, und es beantwortet immer noch jeden Aufruf deiner Tests.
simplify entfernt außerdem anyOf und oneOf aus optionalen Feldern, und das ist der Großteil dessen, was ein Stripe-Antwortschema so mühsam macht. Den Arbeitsspeicher spart der Filter; der Union-Durchlauf macht das Ergebnis lesbar, wenn du es öffnest.In der Service-Konfiguration gibt es den Schalter spec: simplify: true, der den Union-Durchlauf beim Laden macht. Bequem, aber für dieses Problem nutzlos: Das vollständige Dokument muss trotzdem erst gelesen werden, bevor irgendetwas es vereinfachen kann. Kürze die Datei, die du committest.
Den GitHub-Actions-Workflow einrichten
Mit einer Spec, die hineinpasst, ist das Mocken von Stripe in der CI eine Workflow-Datei und sonst nichts. Den Rest erledigt die GitHub Action:
name: mockzilla
on:
push:
branches: [main]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: mockzilla/actions@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}Vorher kein Konto einrichten, nirgends einen Schlüssel einfügen: Der GITHUB_TOKEN belegt, dass das Repository dir gehört. Push das, und die Simulation steht unter https://api.mockz.io/gh/<org>/<repo>, mit deinen sechs Stripe-Endpunkten unter /stripe.
Die Tests darauf richten
Die Action gibt die Adresse als Output zurück:
- uses: mockzilla/actions@v1
id: mockzilla
with:
token: ${{ secrets.GITHUB_TOKEN }}
- run: npm test
env:
STRIPE_BASE_URL: ${{ steps.mockzilla.outputs.url }}/stripeDein Code ruft dann ${STRIPE_BASE_URL}/v1/customers auf statt https://api.stripe.com/v1/customers. In den meisten Codebasen ist das eine Zeile in dem, was den HTTP-Client umschließt.
Rechne damit, dass die erste Antwort langsam ist. Irgendetwas muss die Routen aufbauen, und auf einer Simulation mit kleinem Tarif dauert das einen Moment: rund 1,8 Sekunden, gegenüber einer halben Sekunde für alles danach. Die Action fängt das ab, bevor sie dir die URL gibt, deinen ersten Test kostet es also nichts.
Absichtlich Fehler erzeugen
Eine Simulation, die immer Ja sagt, bringt deinem Retry-Code nichts bei, und genau bei dem bist du dir meistens am wenigsten sicher. Leg eine config.yml neben die Spec:
# services/stripe/config.yml
latency: 120ms
errors:
p2: 500
p5: 429Die Perzentile sind kumulativ: p2: 500 schickt 2 % der Anfragen als 500 zurück, p5: 429 die nächsten 3 % als 429. Für einen einzelnen Aufruf ist ein Header besser, als die Datei zu ändern:
curl -H "X-Mockzilla-Latency: 3s" "$STRIPE_BASE_URL/v1/customers"So testest du den Timeout-Pfad, ohne den Rest der Suite zu bremsen.
Was das nicht kann
Es ist eine Simulation, nicht Stripe.
Die Antworten werden aus der Spec generiert. Sie haben also die richtige Form und die richtigen Statuscodes, sind aber nicht die Daten deines Kontos, und zwischen zwei Aufrufen gibt es keinen Zustand. Ein Kunde, den du anlegst, wartet beim nächsten Lesen nicht auf dich. Wenn deine Tests darauf angewiesen sind, testen sie Stripe und nicht deinen Code, und sie gehören in eine kleinere Suite, die nach Zeitplan gegen die echte Sandbox läuft.
Also trenne sie. Dein eigener Code wird bei jedem Push geprüft, gegen die Spec, die du gekürzt hast. Stripes Verhalten wird nach Zeitplan geprüft, gegen die echte Sandbox, wo ein langsamer Lauf nichts kostet.
Wenn generierte Antworten nicht reichen
Eine Zahlung muss autorisiert sein, bevor sie eingezogen und danach erstattet werden kann, und jeder Schritt muss sich an den vorherigen erinnern. Manche dieser Kombinationen weist Stripe ab, und eine zustandslose Simulation lässt sie durch. Unsere Zahlungs-Sandboxes halten diesen Zustand und bilden die Regeln des jeweiligen PSP ab, mit dessen eigenen Testwerten als Auslöser: Eine Zahlung mit 4000 0000 0000 9995 wird wegen fehlender Deckung abgelehnt. Stripe ist einer davon.
Das Ganze zum Mitnehmen
Alles davon liegt in einem Repository: mockzilla/articles-stripe-in-ci. Der Filter, der Workflow, die Service-Konfiguration und die Tests, die bei jedem Push auf main gegen die deployte Simulation laufen. Die Spec selbst liegt nicht darin: Die CI baut sie jedes Mal aus Stripes veröffentlichtem Dokument.
Installiere die CLI, um denselben services-Ordner auf deinem Rechner laufen zu lassen, bevor du ihn pushst.