Integracja i kompleksowe testy są łatwe dzięki Node.js i MongoDB

Opublikowany: 2022-03-11

Testy są istotną częścią budowania solidnej aplikacji Node.js. Odpowiednie testy mogą z łatwością przezwyciężyć wiele niedociągnięć, które programiści mogą wskazać na rozwiązania deweloperskie Node.js.

Podczas gdy wielu programistów koncentruje się na 100% pokryciu testów jednostkowych, ważne jest, aby pisany przez Ciebie kod nie był testowany tylko w izolacji. Testy integracyjne i testy typu end-to-end zapewniają dodatkową pewność dzięki wspólnemu testowaniu części aplikacji. Te części mogą działać samodzielnie, ale w dużym systemie jednostki kodu rzadko działają osobno.

Node.js i MongoDB tworzą razem jeden z najpopularniejszych duetów ostatnich czasów. Jeśli jesteś jedną z wielu osób, które ich używają, masz szczęście.

W tym artykule dowiesz się, jak łatwo pisać testy integracyjne i kompleksowe dla aplikacji Node.js i MongoDB, które działają na rzeczywistych instancjach bazy danych, a wszystko to bez konieczności konfigurowania skomplikowanego środowiska lub skomplikowanego kodu konfiguracyjnego/destrukcyjnego .

Zobaczysz, jak pakiet mongo-unit pomaga w integracji i testowaniu end-to-end w Node.js. Bardziej kompleksowe omówienie testów integracji Node.js znajdziesz w tym artykule.

Radzenie sobie z prawdziwą bazą danych

Zazwyczaj w przypadku testów integracyjnych lub testów typu end-to-end, Twoje skrypty będą musiały połączyć się z rzeczywistą dedykowaną bazą danych do celów testowych. Obejmuje to pisanie kodu, który jest uruchamiany na początku i na końcu każdego przypadku/pakietu testowego, aby zapewnić, że baza danych jest w czystym przewidywalnym stanie.

Może to działać dobrze w przypadku niektórych projektów, ale ma pewne ograniczenia:

  • Środowisko testowe może być dość złożone. Będziesz musiał gdzieś uruchomić bazę danych. Często wymaga to dodatkowego wysiłku, aby skonfigurować serwery CI.
  • Baza danych i operacje mogą być stosunkowo powolne. Ponieważ baza danych będzie korzystać z połączeń sieciowych, a operacje będą wymagały aktywności systemu plików, szybkie przeprowadzenie tysięcy testów może nie być łatwe.
  • Baza danych utrzymuje stan i nie jest zbyt wygodna do testów. Testy powinny być od siebie niezależne, ale użycie wspólnej bazy danych może spowodować, że jeden test będzie miał wpływ na inne.

Z drugiej strony użycie prawdziwej bazy danych sprawia, że ​​środowisko testowe jest jak najbardziej zbliżone do produkcyjnego. Można to uznać za szczególną zaletę tego podejścia.

Korzystanie z prawdziwej bazy danych w pamięci

Używanie prawdziwej bazy danych do testowania wydaje się wiązać z pewnymi wyzwaniami. Jednak zaleta korzystania z prawdziwej bazy danych jest zbyt duża, aby ją przekazać. Jak możemy obejść wyzwania i zachować przewagę?

Ponowne użycie dobrego rozwiązania z innej platformy i zastosowanie go w świecie Node.js może być drogą do tego.

Projekty Java szeroko wykorzystują w tym celu DUnit z bazą danych w pamięci (np. H2).

DUnit jest zintegrowany z JUnit (program uruchamiający testy Java) i pozwala zdefiniować stan bazy danych dla każdego zestawu testów/testów itp. Usuwa ograniczenia omówione powyżej:

  • DBanit i H2 to biblioteki Java, więc nie musisz konfigurować dodatkowego środowiska. Wszystko działa w JVM.
  • Baza danych w pamięci sprawia, że ​​zarządzanie stanem jest bardzo szybkie.
  • DUnit sprawia, że ​​konfiguracja bazy danych jest bardzo prosta i pozwala zachować przejrzysty stan bazy danych dla każdego przypadku.
  • H2 to baza danych SQL i jest częściowo kompatybilna z MySQL, więc w większości przypadków aplikacja może z nią pracować jak z produkcyjną bazą danych.

Wychodząc z tych koncepcji, postanowiłem zrobić coś podobnego dla Node.js i MongoDB: Mongo-unit.

Mongo-unit to pakiet Node.js, który można zainstalować za pomocą NPM lub Yarn. Działa w pamięci MongoDB. Ułatwia testy integracyjne, dobrze integrując się z Mocha i zapewniając proste API do zarządzania stanem bazy danych.

Biblioteka wykorzystuje prekompilowany pakiet NPM mongodb, który zawiera prekompilowane pliki binarne MongoDB dla popularnych systemów operacyjnych. Te instancje MongoDB mogą działać w trybie w pamięci.

Instalowanie jednostki Mongo

Aby dodać jednostkę mongo do swojego projektu, możesz uruchomić:

 npm install -D mongo-unit

lub

 yarn add mongo-unit

I to jest to. Nie musisz nawet instalować MongoDB na swoim komputerze, aby korzystać z tego pakietu.

Używanie jednostki Mongo do testów integracyjnych

Wyobraźmy sobie, że masz prostą aplikację Node.js do zarządzania zadaniami:

 // service.js const mongoose = require('mongoose') const mongoUrl = process.env.MONGO_URL || 'mongodb://localhost:27017/example' mongoose.connect(mongoUrl) const TaskSchema = new mongoose.Schema({ name: String, started: Date, completed: Boolean, }) const Task = mongoose.model('tasks', TaskSchema) module.exports = { getTasks: () => Task.find(), addTask: data => new Task(data).save(), deleteTask: taskId => Task.findByIdAndRemove(taskId) }

Adres URL połączenia MongoDB nie jest tutaj zakodowany na stałe. Podobnie jak w przypadku większości zapleczy aplikacji internetowych, pobieramy go ze zmiennej środowiskowej. To pozwoli nam zastąpić go dowolnym adresem URL podczas testów.

 const express = require('express') const bodyParser = require('body-parser') const service = require('./service') const app = express() app.use(bodyParser.json()) app.use(express.static(`${__dirname}/static`)) app.get('/example', (req, res) => { service.getTasks().then(tasks => res.json(tasks)) }) app.post('/example', (req, res) => { service.addTask(req.body).then(data => res.json(data)) }) app.delete('/example/:taskId', (req, res) => { service.deleteTask(req.params.taskId).then(data => res.json(data)) }) app.listen(3000, () => console.log('started on port 3000'))

To jest fragment przykładowej aplikacji z interfejsem użytkownika. Kod interfejsu użytkownika został pominięty dla zwięzłości. Możesz sprawdzić pełny przykład na GitHub.

Integracja z Mocha

Aby Mocha uruchamiał testy integracyjne przeciwko mongo-unit, musimy uruchomić instancję bazy danych mongo-unit przed załadowaniem kodu aplikacji w kontekście Node.js. W tym celu możemy wykorzystać parametr mocha --require oraz bibliotekę Mocha-prepare, która pozwala na wykonywanie operacji asynchronicznych w skryptach require.

 // it-helper.js const prepare = require('mocha-prepare') const mongoUnit = require('mongo-unit') prepare(done => mongoUnit.start() .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl done() }))

Pisanie testów integracyjnych

Pierwszym krokiem jest dodanie testu do testowej bazy danych ( testData.json ):

 { "tasks": [ { "name": "test", "started": "2017-08-28T16:07:38.268Z", "completed": false } ] }

Następnym krokiem jest dodanie samych testów:

 const expect = require('chai').expect const mongoose = require('mongoose') const mongoUnit = require('../index') const service = require('./app/service') const testMongoUrl = process.env.MONGO_URL describe('service', () => { const testData = require('./fixtures/testData.json') beforeEach(() => mongoUnit.initDb(testMongoUrl, testData)) afterEach(() => mongoUnit.drop()) it('should find all tasks', () => { return service.getTasks() .then(tasks => { expect(tasks.length).to.equal(1) expect(tasks[0].name).to.equal('test') }) }) it('should create new task', () => { return service.addTask({ name: 'next', completed: false }) .then(task => { expect(task.name).to.equal('next') expect(task.completed).to.equal(false) }) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(2) expect(tasks[1].name).to.equal('next') }) }) it('should remove task', () => { return service.getTasks() .then(tasks => tasks[0]._id) .then(taskId => service.deleteTask(taskId)) .then(() => service.getTasks()) .then(tasks => { expect(tasks.length).to.equal(0) }) }) })

I voila!

Zwróć uwagę, że jest tylko kilka wierszy kodu dotyczących konfiguracji i usuwania.

Jak widać, bardzo łatwo jest pisać testy integracyjne przy użyciu biblioteki mongo-unit. Nie kpimy z samego MongoDB i możemy używać tych samych modeli Mongoose. Mamy pełną kontrolę nad danymi bazy danych i nie tracimy wiele na wydajności testów, ponieważ fałszywa MongoDB działa w pamięci.

Pozwala nam to również zastosować najlepsze praktyki testowania jednostkowego do testów integracyjnych:

  • Uczyń każdy test niezależnym od innych testów. Przed każdym testem ładujemy świeże dane, co daje nam całkowicie niezależny stan dla każdego testu.
  • Użyj minimalnego wymaganego stanu dla każdego testu. Nie musimy wypełniać całej bazy danych. Musimy tylko ustawić minimalne wymagane dane dla każdego konkretnego testu.
  • Możemy ponownie wykorzystać jedno połączenie do bazy danych. Zwiększa wydajność testu.

Jako bonus możemy nawet uruchomić samą aplikację przeciwko mongo-jednostce. Pozwala nam to na wykonywanie testów end-to-end dla naszej aplikacji w oparciu o fałszywą bazę danych.

Kompleksowe testy z selenem

Do testów end-to-end będziemy używać Selenium WebDriver i Hermione E2E.

Najpierw załadujemy kierowcę i uczestnika testowego:

 const mongoUnit = require('mongo-unit') const selenium = require('selenium-standalone') const Hermione = require('hermione') const hermione = new Hermione('./e2e/hermione.conf.js') //hermione config seleniumInstall() //make sure selenium is installed .then(seleniumStart) //start selenium web driver .then(mongoUnit.start) // start mongo unit .then(testMongoUrl => { process.env.MONGO_URL = testMongoUrl //store mongo url }) .then(() => { require('./index.js') //start application }) .then(delay(1000)) // wait a second till application is started .then(() => hermione.run('', hermioneOpts)) // run hermiona e2e tests .then(() => process.exit(0)) .catch(() => process.exit(1))

Będziemy również potrzebować kilku funkcji pomocniczych (obsługa błędów usunięta dla zwięzłości):

 function seleniumInstall() { return new Promise(resolve => selenium.install({}, resolve)) } function seleniumStart() { return new Promise(resolve => selenium.start(resolve)) } function delay(timeout) { return new Promise(resolve => setTimeout(resolve, timeout)) }

Po wypełnieniu bazy danymi i oczyszczeniu jej po zakończeniu testów, możemy uruchomić nasze pierwsze testy:

 const expect = require('chai').expect const co = require('co') const mongoUnit = require('../index') const testMongoUrl = process.env.MONGO_URL const DATA = require('./fixtures/testData.json') const ui = { task: '.task', remove: '.task .remove', name: '#name', date: '#date', addTask: '#addTask' } describe('Tasks', () => { beforeEach(function () { return mongoUnit.initDb(testMongoUrl, DATA) .then(() => this.browser.url('http://localhost:3000')) }) afterEach(() => mongoUnit.dropDb(testMongoUrl)) it('should display list of tasks', function () { const browser = this.browser return co(function* () { const tasks = yield browser.elements(ui.task) expect(tasks.length, 1) }) }) it('should create task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.name).setValue('test') yield browser.element(ui.addTask).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 2) }) }) it('should remove task', function () { const browser = this.browser return co(function* () { yield browser.element(ui.remove).click() const tasks = yield browser.elements(ui.task) expect(tasks.length, 0) }) }) })

Jak widać, testy end-to-end wyglądają bardzo podobnie do testów integracyjnych.

Zakończyć

Integracja i kompleksowe testowanie są ważne dla każdej aplikacji na dużą skalę. W szczególności aplikacje Node.js mogą odnieść ogromne korzyści z automatycznego testowania. Dzięki mongo-unit możesz pisać testy integracyjne i kompleksowe, nie martwiąc się o wszystkie wyzwania związane z takimi testami.

Pełne przykłady korzystania z mongo-unit można znaleźć w serwisie GitHub.

Dalsza lektura na blogu Toptal Engineering:

  • Budowanie interfejsu API REST Node.js/TypeScript, część 3: MongoDB, uwierzytelnianie i testy automatyczne