Testowanie kompatybilności wstecznej API, znane również jako breaking changes testing, to proces sprawdzania, czy wprowadzenie zmian w interfejsie programistycznym (API) nie powoduje przerwania lub negatywnego wpływu na już istniejące aplikacje, systemy lub klientów korzystających z tego API. Breaking changes, czyli zmiany, które mogą zakłócić istniejącą funkcjonalność lub powodować problemy u użytkowników, są kluczowym obszarem testowania w kontekście ewolucji i utrzymania API.
Przyjrzyjmy się kilku aspektom testowania kompatybilności wstecznej API:
Zmiany w strukturze danych: Wprowadzenie zmian w strukturze danych, takie jak dodawanie, usuwanie lub modyfikowanie pól, może mieć wpływ na sposób, w jaki klient odbiera dane. Testowanie powinno sprawdzić, czy istniejące aplikacje nadal mogą poprawnie interpretować nową strukturę danych.
Modyfikacje istniejących zasobów: Zmiany w istniejących endpointach API, takie jak zmiana sposobu działania lub parametrów zapytania, mogą wpłynąć na już istniejące integracje. Testy powinny zweryfikować, czy te zmiany nie powodują błędów u klientów.
Usunięcie funkcji: Jeśli pewne funkcje zostaną usunięte z API, testowanie powinno sprawdzić, czy istniejące aplikacje są świadome tych zmian i nie próbują korzystać z usuniętych funkcji.
Wprowadzając zmiany w API, deweloperzy i zespoły odpowiedzialne za API muszą zadbać o to, aby błędy kompatybilności wstecznej były minimalizowane, odpowiednio zarządzane. Dobre praktyki programistyczne, takie jak stosowanie wersjonowania API, mogą pomóc w zarządzaniu tym procesem i ułatwieniu testowania kompatybilności wstecznej.
W dzisiejszym poście chciałbym przedstawić Wam narzędzia oraz framework, który pozwoli na skuteczne identyfikowanie i weryfikowanie zmian w specyfikacji API, mogących spowodować problemy z kompatybilnością wsteczną dla klientów korzystających z tego API.
Open API Diff
Open API Diff (openapi-diff) to narzędzie, które jest w stanie wykryć różne rodzaje zmian w specyfikacjach API, a w szczególności tzw. breaking changes, czyli zmiany, które mogą wpłynąć na istniejące implementacje klientów API i spowodować problemy z kompatybilnością wsteczną.
Narzędzie jest w stanie zidentyfikować, co zostało zmienione między dwoma plikami specyfikacji Swagger lub OpenAPI. Zmiany te są dzielone na trzy grupy: breaking (łamiące zgodność), non-breaking (niełamiące zgodności) i unclassified (niesklasyfikowane). Korzystając z podejścia opartego na teorii zbiorów, to narzędzie jest w stanie obliczyć te różnice z dużą dokładnością.
Klasyfikacja zmian wykrywanych przez Open API Diff
Zmiany wykryte przez narzędzie Open API Diff mogą byc przypisane do trzech grup:
Breaking (łamiące zgodność)
Zmiany w API, które powodują, iż klienci (consumers) korzystający z tego API działają w sposób niepoprawny.
Usunięcie ścieżki (Path) – Jeśli element, taki jak ścieżka (path), został usunięty w nowej wersji, może to prowadzić do problemów z klientami, którzy korzystają z tego elementu.
Usunięcie metody – Jeśli operacja (np. GET, POST) została usunięta, klientom może brakować oczekiwanej funkcjonalności.
Zmiana właściwości z opcjonalnej na wymaganą w ciele żądania – Jeśli właściwość była wcześniej opcjonalna, istniejące implementacje klientów mogą nie dostarczać tej właściwości w swoich zapytaniach, ponieważ była uznawana za nieobowiązkową.
Zmiana właściwości z wymaganej na opcjonalną w ciele odpowiedzi – Jeśli dana właściwość była wcześniej uznawana za wymaganą w odpowiedzi API, istniejące implementacje klientów mogą polegać na obecności tej właściwości.
Non Breaking (niełamiące zgodność)
Zmiany, które nie sprawią, że istniejący konsumenci staną się niekompatybilni z API, na przykład:
Dodanie ścieżki
Dodanie metody
Zmiana właściwości z wymaganej na opcjonalną w ciele żądania
Zmiana właściwości z opcjonalnej na wymaganą w ciele odpowiedz
Unclassified (niesklasyfikowane)
Zmiany, które zostały wykryte przez narzędzie, ale nie mogą zostać sklasyfikowane, na przykład:
Modyfikacje właściwości X-Properties
Open API Diff – Użycie z CLI
Na samym początku zainstaluj narzędzie za pomocą npm i dodaj je do pliku package.json:
npm install openapi-diff --save-dev
Następnie uruchom narzędzie, podając ścieżki do pliku źródłowego specyfikacji oraz pliku docelowego specyfikacji. Pliki te powinny być w formacie JSON lub YAML i być poprawnymi specyfikacjami Swagger 2 lub OpenAPI 3.
Narzędzie wygeneruje listę różnic łamiących zgodność, niełamiących zgodność oraz niesklasyfikowanych występujących między plikiem źródłowym a docelowym. Jeśli zostaną znalezione jakiekolwiek zmiany łamiące zgodność, narzędzie zwróci kod wyjścia różny od zera.
Jako przykładowe API do testów wykorzystamy serwis API: PetStore.
Zapiszmy teraz zawartość pliku swagger.json powyższego serwisu do dwóch osobnych plików o różnych nazwach np. swagger-groundtruth.json oraz swagger-actual.json
Uruchommy teraz następującą komendę z projektu, w którym zainstalowaliście bibliotekę openapi-diff:
.\node_modules\.bin\openapi-diff.cmd swagger-groundtruth.json swagger-actual.json
Jeśli nie macie ochoty tworzenia własnego projektu, możecie skorzystać z gotowego, przygotowanego na potrzeby tego tutoriala projektu znajdującego się na repozytorium GITHub Automatyzacja.IT. Projekt ten przyda się Wam również do uruchomienia testów opisanych w dalszej części tego wpisu.
Przed uruchomieniem komendy open-api-diff w CLI nie zapomnijcie zainstalować zależności projektu. W tym celu użyjcie komendy npm install
.
Sama komenda uruchamiająca open-api-diff wygląda następująco:
.\node_modules\.bin\openapi-diff.cmd .\schemas\groundtruth\swagger.json .\schemas\actual\swagger.json
Z racji tego, iż oba pliki swagger.json mają tę sama zawartość, w terminalu powinniście otrzymać następującą odpowiedź: „No changes found between the two specifications”. Zalóżmy jednak, iż jeden z deweloperów usunął przypadkowo jedną z funkcji API Petstore. Przykładowo endpoint /user/logout:
"/user/logout": {
"get": {
"tags": [
"user"
],
"summary": "Logs out current logged in user session",
"description": "",
"operationId": "logoutUser",
"produces": [
"application/json",
"application/xml"
],
"parameters": [],
"responses": {
"default": {
"description": "successful operation"
}
}
}
},
Usuńmy go teraz w całości z pliku swagger.json, który znajduję się w folderze schemas/actual, a następnie uruchommy ponownie komendę:
.\node_modules.bin\openapi-diff.cmd .\schemas\groundtruth\swagger.json .\schemas\actual\swagger.json
Tym razem open-api-diff wykrył potencjalny błąd kompatybilności wstecznej, ponieważ usunęliśmy ścieżkę, która mogła być używana przez klienta API:
Open API Diff – Użycie jako API w Node.js
Drugim sposobem użycia narzędzia open-api-diff jest skorzystanie z jego API w Node.js
Aby użyć bibliotekę open-api-diff, należy przekazać SpecOption dla specyfikacji źródłowej i docelowej. Warto nadmienić, iż SpecOption posiada trzy właściwości:
content (string) – ciąg znaków zawierający zserializowaną zawartość w formacie JSON lub YAML
location (string) – nazwa specyfikacji, zwykle ścieżka pliku lub nazwa pliku
format (string) – format specyfikacji – swagger2 albo openapi3
Poniżej znajduje się przykładowe użycie narzędzia jako API w Node.js:
const openapiDiff = require('openapi-diff');
const source = {
// openapi3...
};
const destination = {
// openapi3...
};
const result = await openapiDiff.diffSpecs({
sourceSpec: {
content: JSON.stringify(source),
location: 'source.json',
format: 'openapi3'
},
destinationSpec: {
content: JSON.stringify(destination),
location: 'destination.json',
format: 'openapi3'
}
});
if (result.breakingDifferencesFound) {
console.log('Breaking change found!')
}
Open API Diff – Użycie narzędzia we frameworku
Przejdźmy teraz do przedstawienia autorskiego frameworka, wykorzystującego narzędzie open-api-diff. Tak jak już wcześniej wspomniałem, pełne rozwiązanie projektu możecie znaleźć na GITHub Automatyzacja.IT. Głównym zadaniem frameworka jest testowanie kompatybilności wstecznej API, w tym wypadku jednak do porównania zostanie użyta tkz. groundtruth schema (wzorcowa schema występująca zawsze w postaci pliku) oraz actual schema, która będzie zapisywana do pliku swagger.json w folderze schemas/actual po podaniu wskazanego URL’a danej specyfikacji. W naszym wypadku będzie to: swagger.json
Dodatkowo w celu dokonania bardziej przejrzystej weryfikacji różnic pomiędzy specyfikacjami, użyta została biblioteka json-diff, który służy do porównywania i różnicowania obiektów JSON w języku JavaScript. Te różnice mogą obejmować dodane, usunięte lub zmienione klucze i wartości.
Do zdefiniowania i wykonania samych testów wykorzystany został popularny framework do testowania jednostkowego i akceptacyjnego dla języka JavaScript, czyli mocha.
W celu uruchomienia testów wystarczy wykonać komendę npm run test
, który de facto uruchamia skrypt open-api-diff-test.js:
const jsonAssertion = require("soft-assert");
const util = require("util");
const helper = require("./helper");
const fs = require("fs").promises;
const openApiSchemaName = 'petstore';
const schemaUrl = 'https://petstore.swagger.io/v2/swagger.json';
const dirnameGroundtruthFolderPath = 'schemas/groundtruth/';
const dirnameActualFolderPath = 'schemas/actual/';
describe('Pet Store API OpenAPI difference tests', async function () {
it('Pet Store Test', async function () {
const filenames = await fs.readdir(dirnameGroundtruthFolderPath);
await helper.createActualSchemaFileFromUrl(schemaUrl, dirnameActualFolderPath)
await Promise.all(filenames.map(async (file) => {
const actualFilePath = `${dirnameActualFolderPath}${file}`;
const groundTruthFilePath = `${dirnameGroundtruthFolderPath}${file}`;
try {
const [actualOpenAPI, groundTruthOpenAPI] = await Promise.all([
fs.readFile(actualFilePath, 'utf8').then(JSON.parse),
fs.readFile(groundTruthFilePath, 'utf8').then(JSON.parse)
]);
await helper.createResultsFolderAndSaveSchemas(actualOpenAPI, groundTruthOpenAPI, openApiSchemaName);
const openApiDiffResult = await helper.getOpenApiDiffResult(actualOpenAPI, groundTruthOpenAPI);
const jsonDifferenceResult = await helper.getJsonDifferenceAndSaveResults(actualOpenAPI, groundTruthOpenAPI, openApiSchemaName, file);
jsonAssertion.softAssert(openApiDiffResult.breakingDifferencesFound, false, `Warning! - There is a breaking change in your code - ${file}: ${util.inspect(openApiDiffResult, { depth: null })}`);
jsonAssertion.softAssert(jsonDifferenceResult, undefined, `OpenAPI schema has changed. If change does not break the contract please update the following Groundtruth OpenApi Schema: ${file}`);
} catch (error) {
console.error(`Error processing file ${file}:`, error);
}
}));
jsonAssertion.softAssertAll();
}).timeout(120000);
});
Do wykonania wielu asercji w jednym skrypcie po sobie, bez ryzyka przerwania flow testu, użyta została biblioteka softAssert.
Jak widzicie w skrypcie znajdują się dwie asercje:
jsonAssertion.softAssert(openApiDiffResult.breakingDifferencesFound, false, `Warning! - There is a breaking change in your code - ${file}: ${util.inspect(openApiDiffResult, { depth: null })}`);
jsonAssertion.softAssert(jsonDifferenceResult, undefined, `OpenAPI schema has changed. If change does not break the contract please update the following Groundtruth OpenApi Schema: ${file}`);
Pierwsza sprawdza wystąpienie breaking changes przez narzędzie open-api-diff, druga zaś wykrywa wszelkie zmiany/różnice znalezione przez bibliotekę json-diff.
Przejdźmy teraz do uruchomienia testu. W sytuacji, kiedy obie specyfikacje będą takie same, zakończy się on sukcesem:
Usuńmy teraz endpoint /user/logout z pliku swagger.json znajdującego się w folderze schemas/actual (tak jak zrobiliśmy to poprzednio). Przed uruchomieniem testu jednak zakomentujmy tymczasowo kod znajdujący się w linii 15:
Jeśli pominiemy ten krok, swagger.json mieszczący się w folderze schemas/actual będzie nadpisywany tą samo wersją specyfikacji, której źródłem będzie URL. Po tej zmianie uruchommy jeszcze raz testy za pomocą komendy npm run test
.
Tym razem testy nie zakończyły się sukcesem. Poniżej znajdziemy informacje o niepowodzeniach pochodzących z obu asercji:
- Asercji narzędzia open-api-diff:
- Asercji narzędzia json-diff:
Dodatkowo po zakończeniu testów w folderze results zostaną utworzone trzy pliki w formacie JSON:
petstore-ActualOpenAPI.json – aktualna specyfikacja – jego źródło stanowi URL
petstore-GroundTruthOpenAPI.json – specyfikacja wzorcowa
petstoreDiff.json – plik zawierajacy raport z różnicami wykrytami pomiędzy specyfikacjami, przykład takiego pliku znajduje się poniżej:
To wszystko!😊Od teraz dzięki zastosowaniu podejścia testowania kompatybilności wstecznej API za pomoca narzedzia Open API Diff jesteśmy w stanie skutecznie identyfikować zmiany mogące wpłynąć na istniejące implementacje serwisów.
Aby nie przegapić kolejnego artykułu już teraz zapisz się do Newslettera.