Keresés tartalomra
Kategóriák
Címkék
- Java
- Spring
- Python
- IoC
- Android
- DI
- Dagger
- Thymeleaf
- Markdown
- JDK11
- AOP
- Aspect
- Captcha
- I18n
- JavaSpark
- Microframework
- Testing
- JUnit
- Security
- JWT
- REST
- Database
- JPA
- Gépház
- WebFlux
- ReactiveProgramming
- Microservices
- Continuous Integration
- CircleCI
- Deployment Pipeline
- Docker
- Mocking
- LogProcessing
- PlantUML
- UML
- Modellezés
- OAuth2
- Node.js
- DevOps
- Websocket
- PyPI
- Cucumber
Express.js alkalmazás autotesztelése Cucumber frameworkkel
Alapvetően rögtön egy jó hírrel kezdem a cikket, mert a tesztek felépítése, köszönhetően a Gherkin DSL nyelv-agnosztikus támogatásának, teljesen megegyezik azzal, amit az előző cikkemben bemutattam. Ugyanúgy .feature fájlokat fogunk használni, benne a megszokott Feature/Scenario/Given/When/Then szegmensekkel. Koncepcionálisan tehát nem fogunk tapasztalni eltéréseket, tulajdonképpen a teszteket feldolgozó és futtató framework (vagyis maga a Cucumber, illetve annak a TypeScript implementációja) is pontosan ugyanúgy fogja a scenario lépéseit meghívni és ugyanazokat az előkészületeket is kell megtennünk. Ahol eltérések vannak, az sajnos maga az alkalmazás futtatása lesz, mivel míg a Spring Boot mindezt megoldotta nekünk egy néhány annotációból álló körítéssel, addig a TypeScript-es alkalmazások futtatása némiképp körülményesebb. Van persze lehetőségünk arra, hogy például Docker image-ként csomagolva futtassuk az alkalmazást (ahogy az majd élesben is történni fog), illetve további containerekként biztosítsuk számára a szükséges külső függéseket (például az adatbázist), de ez a build pipelinet megbonyolíthatja, a build futási idejét meghosszabbíthatja, illetve minden egyes tesztfuttatáshoz le kell generálnunk előbb az imaget, ami tipikusan pont a tesztek futtatása után szokott történni. Ehelyett, megpróbálkozhatunk az alkalmazás felpörgetésével ugyanolyan módon, mint a Spring Boot @SpringBootTest annotációja teszi, és most pontosan ezt fogjuk tenni.
Uborkaszezon megint
Először is adjuk hozzá a projekthez a Cucumbert mint dev-dependency: a @cucumber/cucumber csomagra lesz szükségünk (a cikk írásának pillanatában a 12.7.0-s verzió a legfrissebb). Továbbá hozzunk létre egy "script" bejegyzést, ami a teszteket indítja majd; ehhez az npx cucumber-js parancsot kell futtatnunk.
{
// ...
"scripts": {
// ...
"acceptance": "npx cucumber-js"
},
"devDependencies": {
// ..
"@cucumber/cucumber": "12.7.0"
}
}
A következő lépés, hogy a package.json mellett hozzunk létre egy cucumber.yml nevű konfigurációs fájlt, ebben fogunk megadni néhány, a Cucumber működéséhez szükséges paramétert - alább a Leaflet Static Resource Server nevű projektem Cucumber configja látható:
default:
requireModule:
- ts-node/register
paths:
- acceptance/features/**/*.feature
require:
- acceptance/support/init.ts
- acceptance/step-definitions/**/*.ts
tags: not @Disabled
format:
- html:acceptance/out/cucumber-report.html
forceExit: true
A paraméterek a következő direktívákat adják meg:
requireModule: listában adhatunk meg olyan függéseket, melyek szükségesek a Cucumber futásához. A fenti példában láthatóts-node/registermodule lehetővé teszi, hogy a Cucumber step definitionök, és minden egyéb kiegészítő kód (maga az alkalmazás is) közvetlenül futhat TypeScript kódként, JavaScriptre fordítás nélkül.paths: a feature fájlok helyét adhatjuk meg, a fentebb látható módon egy bejegyzéssel többet isrequire: apackage.jsonfájlhoz viszonyított útvonalak, ahol a step definition implementációk, illetve bármilyen egyéb kód, ami az alkalmazás és/vagy a tesztek futtatásához szükséges. A példában láthatóacceptance/support/init.tsutóbbit fogja szolgálni, erre később visszatérünk.tags: a Cucumber definiál egy egyszerű kifejezésgyűjteményt arra, hogy tag-ek szerint tudjuk futtatni a teszteket, ezt a kifejezést adjuk itt meg. A példában láthatónot @Disabledkifejezés gyakorlatilag azt eredményezi majd, hogy minden tag-hez tartozó (és nem tag-elt) tesztet lefuttat a framework, kivéve a@Disabled-ként megjelölteket.format: a Cucumber a tesztfuttatás végén generál egy riportot minden lefuttatott tesztről. A fenti beállításhtmlreportot fog generálni a kettőspont után megadott célútvonalra.forceExit: amiatt, ahogyan az alkalmazás futni fog a tesztek alatt (gyakorlatilag az Express szerver a tesztek lefutása után is tovább figyel, így futva maradna), a Cucumber fogja kényszerítve leállítani ennek a kapcsolónak köszönhetően.
További paraméterek is elérhetőek, a Cucumber JS projekt GitHubján.
Step definition
Korábban már láttuk, hogyan kell a step definition fájlokat megírni Java nyelven, most ugyanezt TypeScriptben kell megtennünk. A jó hír, hogy látványos különbséget csak a két nyelv eltérő szintaxisa okoz. A step definition implementációkat a korábban a konfigurációban megadott útvonalon levő .ts fájlokban kell megírnunk.
// Ugyanúgy reguláris kifejezésekkel fogunk paramétereket definiálni.
// Az alábbi definícióra illeszkedni fog a következő lépés egy .feature fájlban:
// Given the user is authorized to read:something
Given(/^the user is authorized to ([a-z: ]+)$/, async (scope: string) => {
const mappedScope = scope.split(" ").map(scope => scopeMap.get(scope)!);
DataRegistry.put(Attribute.AUTHORIZATION_HEADER, await AuthManager.createAuthorizationHeader(mappedScope));
});
// Ez pedig a következő mondatra:
// When calling the retrieve files endpoint
When("calling the retrieve files endpoint", async () => {
DataRegistry.putResponse(await restClient.callGetUploadedFilesEndpoint());
});
// Illetve ez a következő mondatra és táblázatra:
// And the following file metadata is returned
// | Reference | Path UUID | ...
// | /d4b1830d-f368-37a0-88f9-2faf7fa8ded6/stored_filename_1.jpg | d4b1830d-f368-37a0-88f9-2faf7fa8ded6 | ...
// Itt a táblázatból a `DataTable` tudja összeszedni az adatokat.
Then("the following file metadata is returned", (expectedMetadata: DataTable) => {
const returnedFileModel = DataRegistry.getResponse<FileModel>().data;
const expectedFileModel = convertFileModel(expectedMetadata.rows()[0]);
expect(returnedFileModel).toStrictEqual(expectedFileModel);
});
Az elvárt eredmények asszertálására a Jest framework API-ját használhatjuk.
Mint korábban Java-ban, itt is tudunk megadni olyan lépéseket, amiket a framework meg kell ismételjen minden egyes tesztfuttatás előtt, után, vagy a teljes tesztkészlet előtt vagy után egyszer.
// Ez egy nagyon fontos lépés lesz céljaink tekintetében, hiszen ez a direktíva fogja elindítani az alkalmazást,
// felépíteni a tesztadatbázist, és egy mockolt OAuth Authorization Servert, hogy a jogosultságokat is tesztelni tudjuk.
BeforeAll({timeout: 10000}, async () => {
await ApplicationManager.start();
await DatasourceManager.reInitDatabase();
await AuthManager.initAuthorizerMock();
});
// Ez pedig az összes teszt lefutása után végez takarítást.
AfterAll(async () => {
await AuthManager.stopAuthorizerMock();
ApplicationManager.cleanUp();
});
// Ahogy a Java implementáció esetében, itt is kell egy módszer az adatok megosztására a tesztlépések között.
// Az alábbi lépés minden scenario után törli a megosztott adatokat.
After(() => {
DataRegistry.reset();
});
Az alkalmazás elindítása
A legnagyobb kihívás továbbra is az, hogy elindítsuk az alkalmazást. Ehhez gyakorlatilag fel kell pörgetnünk az Express.js szerverét, ami mint "éles" esetben is, nyit egy várakozó HTTP portot, ezzel nyitva tartva a processzt. Miután ez megtörtént, be kell töltenünk a tesztadatokat az adatbázisba, illetve hasonló módon vagy mockok használatával el kell indítanunk minden további függést. Ehhez én az alábbi "support" scripteket hoztam létre (a teljes implementáció megtekinthető az LSRS projekt /acceptance/support mappájában:
init.ts
Ezt a fájlt már korábban említettem, direkt hivatkoztunk rá a Cucumber konfigurációjában. Mielőtt elindítanám magát az alkalmazást, be kell állítanom bizonyos környezeti változókat, ez a fájl pedig éppen ezt teszi.
export const uploadPath = `${os.tmpdir()}/lsrs-storage`;
export const databasePath = `acceptance/out/lsrs-sqlite.db`;
process.env.NODE_ENV = "test";
process.env.NODE_CONFIG = JSON.stringify({
lsrs: {
datasource: {
uri: `sqlite:${databasePath}`
},
storage: {
"upload-path": uploadPath
}
}
});
application-manager.ts
Itt található az alkalmazás elindításához szükséges logika, illetve ugyanide raktam a tesztek futtatása utáni takarításhoz szükséges kódot is. A legfontosabb természetesen azApplicationManagerosztálystart()metódusa, mely dinamikus import használatával betölti az alkalmazás eredeti belépési pontját, ezzel elindítva a kontextus felépítését, illetve annak végén az Express HTTP szerverének elindítását. Ezt a metódus hívja a korábban már említettBeforeAll()Cucumber direktíva, így elindítva az alkalmazás egy példányát, mielőtt bármilyen teszt lefutna.
public static async start(): Promise<void> {
return new Promise((resolve) => {
// Ha a Cucumber megint ráhívna a start() metódusra, ez a flag megakadályozza,
// hogy még egy példányban elinduljon az alkalmazás
if (this.started) {
this.notifyStarted(resolve, "Application is already loaded");
} else {
this.logger.info(`Starting application with environment=${process.env.NODE_ENV}...`);
this.started = true;
// Ez a sor indítja el a kontextus felépítését és a HTTP szervert
import("../../src/lsrs-main");
setTimeout(() => {
this.prepareMockStorage();
this.notifyStarted(resolve, "Assuming application is now running");
}, this.startUpTimeout);
}
});
}
auth-manager.ts
Ez egy OAuth Authorization Server mockot fog indítani a jogosultságok tesztelésére. Ha ilyenre is szükségünk van, csak adjuk hozzá a projektdev-dependency-jeihez azoauth2-mock-servercsomagot, aztán már használhatjuk is. A mock képes tokenek kiállítására, illetve figyel a token verifikációs kérésekre is, így teljesen úgy működik, mint egy igazi OAuth szerver.data-registry.ts
Egy hasonló tesztlépések közötti adatmegosztó implementáció, mint amit a Java-s teszteknél láthattunk korábban. Ez a naív implementáció egy közös map-et használ, így amennyiben párhuzamosan szeretnénk futtatni a teszteket, figyelni kell az izolációra, és szükség lehet egyAsyncLocalStoragealapú megoldásra. A scenario-k után lefutóDataRegistry.reset()hívás ezt az átmeneti tárat üríti.datasource-manager.ts
Ez a script a mock-adatbázisunk kezelésére szolgál. ADataSourceManager.reInitDatabase()hívás egy új adatbáziskapcsolatot nyit aSequelizeORM és SQLite adatbázis használatával. Ha már létezne, akkor törli és újra betölti a tesztadatokat (az/acceptance/support/sqlmappában levő SQL scriptek segítségével), ezzel minden hívásnál tiszta állapotra állítva a tesztadatbázist. Erre is van egy step definition-ünk, ami a@DirtiesDatabasetag jelenlétét figyeli, és ha az jelen van egy scenarion, akkor a lefutása után letakarítja az adatbázist:
After({tags: "@DirtiesDatabase"}, async () => {
await DatasourceManager.reInitDatabase();
});
Bárhol egy .feature fájlban:
@PositiveScenario
@DirtiesDatabase
Scenario: Updating the metadata of an existing file
lsrs-rest-client.ts
Axios alapú REST kliens hívások gyűjteménye található ebben a fájlban, hogy ne kell a request definitionöket szétszórni a step definition fájlokban.
Ha mindent jól összedrótoztunk, a Cucumber elindítja az alkalmazást, feltölti a tesztadatbázist, létrehoz egy mock OAuth szervert, majd lefuttatja a teszteket, végül leállít mindent, majd kényszerítve leállítja az alkalmazást és a Cucumber maga is kilép.

Komment írásához jelentkezz be
Bejelentkezés

Még senki nem szólt hozzá ehhez a bejegyzéshez.