Warten Sie nicht! Mocken Sie die API

Avatar of Marko Ilic
Marko Ilic on

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Heute haben wir eine lose Kopplung zwischen dem Frontend und dem Backend von Webanwendungen. Sie werden normalerweise von separaten Teams entwickelt, und es ist nicht einfach, diese Teams und die Technologie synchron zu halten. Um einen Teil dieses Problems zu lösen, können wir den API-Server „faken“, den die Backend-Technologie normalerweise erstellen würde, und so entwickeln, als ob die API oder Endpunkte bereits existieren.

Der gebräuchlichste Begriff für die Erstellung simulierter oder „gefakter“ Komponenten ist Mocking. Mocking ermöglicht es Ihnen, die API zu simulieren, ohne (idealerweise) das Frontend zu ändern. Es gibt viele Möglichkeiten, Mocking zu erreichen, und das ist es, was es für die meisten Leute beängstigend macht, zumindest meiner Meinung nach. 

Lassen Sie uns behandeln, wie ein gutes API-Mocking aussehen sollte und wie man eine gemockte API in eine neue oder bestehende Anwendung implementiert.

Beachten Sie, dass die Implementierung, die ich gleich zeigen werde, Framework-agnostisch ist – sie kann also mit jedem Framework oder einer reinen JavaScript-Anwendung verwendet werden.

Mirage: Das Mocking-Framework

Der Mocking-Ansatz, den wir verwenden werden, heißt Mirage, der noch relativ neu ist. Ich habe viele Mocking-Frameworks getestet und dieses erst kürzlich entdeckt, und es war ein Game Changer für mich.

Mirage wird als frontend-freundliches Framework vermarktet, das mit einer modernen Benutzeroberfläche ausgestattet ist. Es funktioniert in Ihrem Browser, serverseitig, indem es XMLHttpRequest und Fetch-Anfragen abfängt.

Wir werden eine einfache Anwendung mit einer gemockten API erstellen und dabei einige häufige Probleme behandeln.

Mirage-Setup

Lassen Sie uns eine dieser Standard-To-Do-Anwendungen erstellen, um Mocking zu demonstrieren. Ich werde Vue als mein bevorzugtes Framework verwenden, aber natürlich können Sie auch etwas anderes verwenden, da wir mit einem Framework-agnostischen Ansatz arbeiten.

Installieren Sie also Mirage in Ihrem Projekt

# Using npm
npm i miragejs -D


# Using Yarn
yarn add miragejs -D

Um Mirage nutzen zu können, müssen wir einen „Server“ einrichten (in Anführungszeichen, da es ein Fake-Server ist). Bevor wir mit dem Setup beginnen, behandle ich die Ordnerstruktur, die sich meiner Meinung nach am besten eignet.

/
├── public
├── src
│   ├── api
│   │   └── mock
│   │       ├── fixtures
│   │       │   └── get-tasks.js
│   │       └── index.js
│   └── main.js
├── package.json
└── package-lock.json

Öffnen Sie in einem mock-Verzeichnis eine neue index.js-Datei und definieren Sie Ihren Mock-Server

// api/mock/index.js
import { Server } from 'miragejs';


export default function ({ environment = 'development' } = {}) {
  return new Server({
    environment,


    routes() {
      // We will add our routes here
    },
  });
}

Das Environment-Argument, das wir der Funktionssignatur hinzufügen, ist nur eine Konvention. Wir können bei Bedarf eine andere Umgebung übergeben.

Öffnen Sie nun Ihre App-Bootstrap-Datei. In unserem Fall ist dies die Datei src/main.js, da wir mit Vue arbeiten. Importieren Sie Ihre createServer-Funktion und rufen Sie sie in der Entwicklungsumgebung auf.

// main.js
import createServer from './mock'


if (process.env.NODE_ENV === 'development') {
    createServer();
}

Wir verwenden hier die Umgebungsvariable process.env.NODE_ENV, die eine gängige globale Variable ist. Die Bedingung ermöglicht es Mirage, in der Produktion „tree-shaken“ zu werden, daher wird Ihr Produktionsbundle nicht beeinträchtigt.

Das ist alles, was wir für das Setup von Mirage brauchen! Diese Einfachheit macht die DX von Mirage so angenehm.

Unsere createServer-Funktion verwendet standardmäßig die Umgebung development, um diesen Artikel einfach zu halten. In den meisten Fällen wird dies standardmäßig auf test gesetzt, da Sie in den meisten Apps createServer einmal im Entwicklungsmodus, aber viele Male in Testdateien aufrufen werden.

Wie es funktioniert

Bevor wir unsere erste Anfrage stellen, werfen wir einen kurzen Blick darauf, wie Mirage funktioniert.

Mirage ist ein clientseitiges Mocking-Framework, d. h. das gesamte Mocking findet im Browser statt, was Mirage mit der Pretender-Bibliothek tut. Pretender ersetzt vorübergehend native XMLHttpRequest- und Fetch-Konfigurationen, fängt alle Anfragen ab und leitet sie an einen kleinen Schein-Service weiter, an den sich Mirage hängt.

Wenn Sie DevTools öffnen und zum Netzwerk-Tab wechseln, sehen Sie keine Mirage-Anfragen. Das liegt daran, dass die Anfrage von Mirage (über Pretender im Backend) abgefangen und verarbeitet wird. Mirage protokolliert alle Anfragen, was wir gleich behandeln werden.

Lassen Sie uns Anfragen stellen!

Lassen Sie uns eine Anfrage an einen /api/tasks-Endpunkt erstellen, der eine Liste von Aufgaben zurückgibt, die wir in unserer To-Do-App anzeigen werden. Beachten Sie, dass ich axios verwende, um die Daten abzurufen. Das ist nur meine persönliche Präferenz. Auch hier funktioniert Mirage mit nativen XMLHttpRequest, Fetch und jeder anderen Bibliothek.

// components/tasks.vue
export default {
  async created() {
    try {
      const { data } = await axios.get('/api/tasks'); // Fetch the data
      this.tasks = data.tasks;
    } catch(e) {
      console.error(e);
    }
  }
};

Wenn Sie Ihre JavaScript-Konsole öffnen, sollte dort ein Fehler von Mirage erscheinen

Mirage: Your app tried to GET '/api/tasks', but there was no route defined to handle this request.

Das bedeutet, dass Mirage läuft, aber der Router noch nicht gemockt wurde. Lassen Sie uns das lösen, indem wir diese Route hinzufügen.

Anfragen mocken

In unserer Datei mock/index.js gibt es einen routes()-Hook. Routen-Handler ermöglichen es uns zu definieren, welche URLs vom Mirage-Server behandelt werden sollen.

Um einen Routen-Handler zu definieren, müssen wir ihn innerhalb der routes()-Funktion hinzufügen.

// mock/index.js
export default function ({ environment = 'development' } = {}) {
    // ...
    routes() {
      this.get('/api/tasks', () => ({
        tasks: [
          { id: 1, text: "Feed the cat" },
          { id: 2, text: "Wash the dishes" },
          //...
        ],
      }))
    },
  });
}

Der routes()-Hook ist die Art und Weise, wie wir unsere Routen-Handler definieren. Die Verwendung der Methode this.get() ermöglicht es uns, GET-Anfragen zu mocken. Das erste Argument aller Anfragefunktionen ist die URL, die wir behandeln, und das zweite Argument ist eine Funktion, die mit einigen Daten antwortet.

Als Hinweis: Mirage akzeptiert jeden HTTP-Anfragetyp, und jeder Typ hat die gleiche Signatur

this.get('/tasks', (schema, request) => { ... });
this.post('/tasks', (schema, request) => { ... });
this.patch('/tasks/:id', (schema, request) => { ... });
this.put('/tasks/:id', (schema, request) => { ... });
this.del('/tasks/:id', (schema, request) => { ... });
this.options('/tasks', (schema, request) => { ... });

Wir werden die Parameter schema und request der Callback-Funktion in Kürze besprechen.

Damit haben wir unsere Route erfolgreich gemockt und sollten in unserer Konsole eine erfolgreiche Antwort von Mirage sehen.

Screenshot of a Mirage response in the console showing data for two task objects with IDs 1 and 2.

Arbeiten mit dynamischen Daten

Der Versuch, eine neue To-Do-Aufgabe zu unserer App hinzuzufügen, wird nicht möglich sein, da unsere Daten in der GET-Antwort hartcodierte Werte haben. Mirages Lösung dafür ist, dass sie eine leichtgewichtige Datenschicht bereitstellen, die wie eine Datenbank fungiert. Lassen Sie uns das, was wir bisher haben, korrigieren.

Ähnlich wie beim routes()-Hook definiert Mirage einen seeds()-Hook. Er ermöglicht es uns, initiale Daten für den Server zu erstellen. Ich werde die GET-Daten in den seeds()-Hook verschieben, wo ich sie in die Mirage-Datenbank einfügen werde.

seeds(server) {
  server.db.loadData({
    tasks: [
      { id: 1, text: "Feed the cat" },
      { id: 2, text: "Wash the dishes" },
    ],
  })
},

Ich habe unsere statischen Daten aus der GET-Methode in den seeds()-Hook verschoben, wo diese Daten in eine Fake-Datenbank geladen werden. Nun müssen wir unsere GET-Methode refaktorisieren, um Daten aus dieser Datenbank zurückzugeben. Das ist eigentlich ziemlich einfach – das erste Argument der Callback-Funktion jeder route()-Methode ist das Schema.

this.get('/api/tasks', (schema) => {
  return schema.db.tasks;
})

Jetzt können wir neue To-Do-Elemente zu unserer App hinzufügen, indem wir eine POST-Anfrage stellen

async addTask() {
  const { data } = await axios.post('/api/tasks', { data: this.newTask });
  this.tasks.push(data);
  this.newTask = {};
},

Wir mocken diese Route in Mirage, indem wir einen POST /api/tasks-Routen-Handler erstellen

this.post('/tasks', (schema, request) => {})

Über den zweiten Parameter der Callback-Funktion können wir die gesendete Anfrage sehen.

Screenshot of the mocking server request. The requestBody property is highlighted in yellow and contains text data that says Hello CSS-Tricks.

Innerhalb der requestBody-Eigenschaft befinden sich die von uns gesendeten Daten. Das bedeutet, sie stehen uns nun zur Verfügung, um eine neue Aufgabe zu erstellen.

this.post('/api/tasks', (schema, request) => {
  // Take the send data from axios.
  const task = JSON.parse(request.requestBody).data


  return schema.db.tasks.insert(task)
})

Die id der Aufgabe wird standardmäßig von Mirages Datenbank festgelegt. Daher ist es nicht notwendig, IDs zu verfolgen und sie mit Ihrer Anfrage zu senden – genau wie bei einem echten Server.

Dynamische Routen? Klar!

Das Letzte, was wir behandeln müssen, sind dynamische Routen. Sie ermöglichen uns die Verwendung eines dynamischen Segments in unserer URL, was nützlich zum Löschen oder Aktualisieren eines einzelnen To-Do-Elements in unserer App ist.

Unsere Löschanfrage sollte an /api/tasks/1, /api/tasks/2 und so weiter gehen. Mirage bietet uns eine Möglichkeit, ein dynamisches Segment in der URL zu definieren, wie folgt

this.delete('/api/tasks/:id', (schema, request) => {
  // Return the ID from URL.
  const id = request.params.id;


  return schema.db.tasks.remove(id);
})

Die Verwendung eines Doppelpunkts (:) in der URL ist die Art und Weise, wie wir ein dynamisches Segment in unserer URL definieren. Nach dem Doppelpunkt geben wir den Namen des Segments an, das in unserem Fall id heißt und der ID eines bestimmten To-Do-Elements zugeordnet ist. Wir können auf den Wert des Segments über das request.params-Objekt zugreifen, wobei der Eigenschaftsname dem Segmentnamen entspricht – request.params.id. Dann verwenden wir das Schema, um ein Element mit derselben ID aus der Mirage-Datenbank zu entfernen.

Wenn Sie bemerkt haben, sind alle meine bisherigen Routen mit api/ präfixiert. Das wiederholte Schreiben kann umständlich sein und Sie möchten es vielleicht einfacher machen. Mirage bietet die Eigenschaft namespace, die helfen kann. Innerhalb des Routen-Hooks können wir die Eigenschaft namespace definieren, damit wir sie nicht jedes Mal schreiben müssen.

routes() {
 // Prefix for all routes.
 this.namespace = '/api';


 this.get('/tasks', () => { ... })
 this.delete('/tasks/:id', () => { ... })
 this.post('/tasks', () => { ... })
}

OK, integrieren wir das in eine bestehende App

Bisher haben wir Mirage in eine neue App integriert. Aber was ist mit dem Hinzufügen von Mirage zu einer bestehenden Anwendung? Mirage kümmert sich darum, damit Sie nicht Ihre gesamte API mocken müssen.

Das erste, was zu beachten ist, ist, dass das Hinzufügen von Mirage zu einer bestehenden Anwendung einen Fehler auslöst, wenn die Seite eine Anfrage stellt, die nicht von Mirage behandelt wird. Um dies zu vermeiden, können wir Mirage anweisen, alle unbehandelten Anfragen weiterzuleiten.

routes() {
  this.get('/tasks', () => { ... })
  
  // Pass through all unhandled requests.
  this.passthrough()
}

Jetzt können wir auf einer bestehenden API entwickeln, wobei Mirage nur die fehlenden Teile unserer API behandelt.

Mirage kann sogar die Basis-URL ändern, von der aus es Anfragen abfängt. Das ist nützlich, da ein Server normalerweise nicht unter localhost:3000 läuft, sondern unter einer benutzerdefinierten Domain.

routes() {
 // Set the base route.
 this.urlPrefix = 'https://devenv.ourapp.example';


 this.get('/tasks', () => { ... })
}

Jetzt werden alle unsere Anfragen auf den echten API-Server zeigen, aber Mirage wird sie abfangen, wie es das getan hat, als wir es mit einer neuen App eingerichtet haben. Das bedeutet, dass der Übergang von Mirage zur echten API ziemlich nahtlos ist – löschen Sie die Route vom Mock-Server und die Dinge sind einsatzbereit.

Zusammenfassung

Im Laufe von fünf Jahren habe ich viele Mocking-Frameworks verwendet, aber ich mochte nie wirklich eine der vorhandenen Lösungen. Bis vor kurzem, als mein Team eine Mocking-Lösung benötigte und ich Mirage entdeckte.

Andere Lösungen, wie der weit verbreitete JSON-Server, sind externe Prozesse, die neben dem Frontend laufen müssen. Darüber hinaus sind sie oft nichts weiter als ein Express-Server mit Utility-Funktionen obendrauf. Das Ergebnis ist, dass Frontend-Entwickler wie wir etwas über Middleware, NodeJS und wie Server funktionieren, wissen müssen… Dinge, mit denen viele von uns wahrscheinlich nichts zu tun haben wollen. Andere Versuche, wie Mockoon, haben eine komplexe Benutzeroberfläche und fehlen dringend benötigte Funktionen. Es gibt eine weitere Gruppe von Frameworks, die nur zum Testen verwendet werden, wie das beliebte SinonJS. Leider können diese Frameworks nicht verwendet werden, um das normale Verhalten zu mocken.

Mein Team konnte einen funktionierenden Server erstellen, der es uns ermöglicht, Frontend-Code zu schreiben, als ob wir mit einem echten Backend arbeiten würden. Wir haben dies erreicht, indem wir die Frontend-Codebasis ohne externe Prozesse oder Server geschrieben haben, die zum Ausführen benötigt werden. Deshalb liebe ich Mirage. Es ist sehr einfach einzurichten und dennoch leistungsfähig genug, um alles zu bewältigen, was ihm begegnet. Sie können es für einfache Anwendungen, die ein statisches Array zurückgeben, bis hin zu vollwertigen Backend-Apps verwenden – unabhängig davon, ob es sich um eine neue oder eine bestehende App handelt.

Es gibt noch viel mehr zu Mirage jenseits der hier behandelten Implementierungen. Ein funktionierendes Beispiel dessen, was wir behandelt haben, finden Sie auf GitHub. (Fun Fact: Mirage funktioniert auch mit GraphQL!) Mirage hat gut geschriebene Dokumentation, die viele Schritt-für-Schritt-Tutorials enthält, also schauen Sie sich diese unbedingt an.