Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Express.js alkalmazás autotesztelése Cucumber frameworkkel

Szűrő megjelenítése

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/register module 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 is
  • require: a package.json fá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.ts utó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 @Disabled kifejezé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ás html reportot 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 az ApplicationManager osztály start() 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ített BeforeAll() 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 projekt dev-dependency-jeihez az oauth2-mock-server csomagot, 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 egy AsyncLocalStorage alapú 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. A DataSourceManager.reInitDatabase() hívás egy új adatbáziskapcsolatot nyit a Sequelize ORM és SQLite adatbázis használatával. Ha már létezne, akkor törli és újra betölti a tesztadatokat (az /acceptance/support/sql mappá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 @DirtiesDatabase tag 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.

alt

Kommentek

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

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