NodeJS: Jak testować MongoDB za pomocą MongoMemoryServer?

Opublikowano: - tagi:

Co to jest MongoMemoryServer?

Pisanie testów na backendzie często wiąże się z testowaniem rzeczy związanych z bazą danych. Jak to można stestować? Łączenie się z bazą danych to słabe rozwiązanie, ponieważ zajmuje to trochę czasu, więc testy byłyby wolne. Można użyć na przykład mocków. Osobiście jestem za tym, żeby mocków używać jak najmniej, ponieważ chciałbym widzieć, jak się wszystko spina razem — odzwierciedla to prawdziwe działanie skryptu. Dlatego, to co można jeszcze zrobić to przechowywać dane w pamięci.

Tym zajmuje się biblioteka MongoMemoryServer. Nietrudno się domyślić, że jest to biblioteka do obsługi bazy danych MongoDB.

W tym wpisie opiszę konfigurację i podam prosty przykład jej użycia.

Konfiguracja MongoMemoryServer

Zanim zaczniemy, w tym wpisie zakładam, że używamy:

  1. Mongoose — Jest to biblioteka do obsługi MongoDB.
  2. TypeScript — Podaję kod w TypeScript. Tutaj znajdziesz wpis jak podpiąć TypeScript w NodeJS
  3. Jest — Do obsługi testów używam frameworka Jest. Z tego wpisu dowiesz się jak skonfigurować Jest

Instalacja MongoMemoryServer

Wywołaj komendę:

npm i -D mongodb-memory-server

Plik konfiguracyjny

Tworzymy plik konfiguracyjny: db.ts:

import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";

let mongoServer: MongoMemoryServer;

const connect = async () => {
    mongoServer = await MongoMemoryServer.create();
    await mongoose.connect(mongoServer.getUri(), { useNewUrlParser: true,  useUnifiedTopology: true });
};

const close = async () => {
    await mongoose.connection.dropDatabase();
    await mongoose.connection.close();
    await mongoose.disconnect();
    await mongoServer.stop();
};

const clear = async () => {
    const collections = mongoose.connection.collections;
    for (const key in collections) {
        await collections[key].deleteMany({});
    }
};

export default { connect, close, clear };

Mamy trzy funkcję:

  1. connect — Łączymy się z bazą danych
  2. close — Zamykamy połączenie z bazą
  3. clear — Czyścimy dane, w danej kolekcji

Mamy konfigurację czas na sprawdzenie, czy to działa.

Model danych

Stwórzmy model danych: ingredient.ts:

import { model, Schema } from 'mongoose';

export interface Ingredient {
    name: string;
    unit: string;
}

const IngredientSchema = new Schema<Ingredient>({
    name: { type: String, required: true },
    unit: { type: String, required: true }
});

export const IngredientModel = model('Ingredient', IngredientSchema);

Pisanie testów

Stwórzmy plik: ingredient.spec.ts:

import db from "./db";
import {Ingredient, IngredientModel} from "./ingredient";

beforeAll(async () => await db.connect());

afterEach(async () => await db.clear());

afterAll(async () => await db.close());

test('should fetch all ingredients', async () => {
    // given
    const ingredients: Ingredient[] = [
        {
            name: 'Test 1',
            unit: 'g'
        },
        {
            name: 'Test 2',
            unit: 'l'
        },
        {
            name: 'Test 3',
            unit: 'tsp'
        }
    ];
    await IngredientModel.create(ingredients);

    // when
    const fetchedIngredients: Ingredient[] = await IngredientModel.find({});

    // then
    expect(fetchedIngredients.length).toEqual(3);
});

test('should create ingredient', async () => {
    // given
    const ingredient: Ingredient = {
        name: 'Test 1',
        unit: 'g'
    };

    // when
    const createdIngredient: Ingredient = await IngredientModel.create(ingredient);

    // then
    expect(createdIngredient.name).toEqual(ingredient.name);
    expect(createdIngredient.unit).toEqual(ingredient.unit);
});

W beforeAll, afterAll nawiązujemy połączenie lub je zamykamy z bazą danych, a w afterEach czyścimy dane przed uruchomienie kolejnego testu.

Uruchomienie testów

W pliku package.json dodaj:

"scripts": {
	"test": "jest --runInBand --detectOpenHandles"
}

Uruchom następnie testy:

npm run test

RxJS: Operator exhaustMap

Opublikowano: - tagi:

Do czego służy exhaustMap?

Operator exhaustMap pobiera dane z podanego źródła typu Observable. Ignoruje odbiór kolejnych danych do momentu, gdy dane z aktualnego źródła zostaną pobrane.

Przykład użycia exhaustMap

import { fromEvent, of } from 'rxjs';
import { exhaustMap, delay, map } from 'rxjs/operators';

const click$ = fromEvent(document, 'click');

click$.pipe(
    exhaustMap((event) => of(event).pipe(
        map(event => ({
          x: event.clientX,
          y: event.clientY
        })),
        delay(1000)
    )),
).subscribe(console.log)

Kliknij szybko kilka razy gdziekolwiek na stronie. Zostanie odebrany tylko jeden sygnał. Dlaczego? Ponieważ tak jak napisałem wyżej exhaustMap czeka na zakończenie aktualnej emisji danych. Dopiero po odebraniu danych pobierze następne.

exhaustMap jest podobony do operatora concatMap. Podmień w tym przykładzie exhaustMap na concatMap, żeby zobaczyć różnicę między nimi.


Comics: I am your father!

Opublikowano: - tagi:

Podsumowanie: Październik 2022

Opublikowano: - tagi:

W październiku opublikowałem 7 wpisów:


RxJS:

  1. Operator mergeMap
  2. Operator switchMap
  3. Operator concatMap

JavaScript:

  1. Jest: Podstawowa konfiguracja
  2. Jak dodać wiele elementów do tablicy?

Narysowałem dwa komiksy:

  1. Code - The Good, The Bad, The Ugly
  2. Technical debt

Przeczytałem trzy książki:

  1. Żar - Dariusz Rosiak
  2. To nie jest kraj dla starych ludzi - Cormac McCarthy
  3. Dizajn na co dzień - Don Norman

Przesłuchałem dwa audiobooki:

  1. Discipline is destiny - Ryan Holiday
  2. Baśniowa opowieść - Stephen King

RxJS: Operator concatMap

Opublikowano: - tagi:

Operator concatMap służy do pobrania danych ze źródła typu Observable.

Działa w taki sposób, że czeka, aż zakończy się aktualne wywołanie z podanego źródła i wywołuje ewentualnie następną emisję danych.

Jeśli zależy Ci na kolejności wywołań z danego źródła użyj concatMap.

Przykład

import { fromEvent, of } from 'rxjs';
import { concatMap, delay, map } from 'rxjs/operators';

const click$ = fromEvent(document, 'click');

click$.pipe(
    concatMap((event) => of(event).pipe(
        map(event => ({
          x: event.clientX,
          y: event.clientY
        })),
        delay(1000)
    )),
).subscribe(console.log)

Kliknij gdziekolwiek na stronie dwa razy. Co sekundę będą wyświetlane koordynaty poszczególnego kliknięcia. W podanej kolejności: pierwsze kliknięcie, drugie itd.