Jak jedna jednostka testuje trasy za pomocą usługi Express?

99

Jestem w trakcie nauki Node.js i bawiłem się Expressem . Bardzo podoba mi się framework; jednak mam problem ze zrozumieniem, jak napisać test jednostkowy / integracyjny dla trasy.

Możliwość jednostkowego testowania prostych modułów jest łatwa i robiłem to z Mocha ; Jednak moje testy jednostkowe z Express kończą się niepowodzeniem, ponieważ obiekt odpowiedzi, który przekazuję, nie zachowuje wartości.

Trasa-funkcja w trakcie testowania (route / index.js):

exports.index = function(req, res){
  res.render('index', { title: 'Express' })
};

Moduł testów jednostkowych:

var should = require("should")
    , routes = require("../routes");

var request = {};
var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        viewName = view;
        data = viewData;
    }
};

describe("Routing", function(){
    describe("Default Route", function(){
        it("should provide the a title and the index view name", function(){
        routes.index(request, response);
        response.viewName.should.equal("index");
        });

    });
});

Po uruchomieniu to kończy się niepowodzeniem z komunikatem „Błąd: wykryto globalne wycieki: nazwa_widoku, dane”.

  1. Gdzie popełniam błąd, aby to zadziałało?

  2. Czy istnieje lepszy sposób na testowanie jednostkowe kodu na tym poziomie?

Aktualizacja 1. Poprawiony fragment kodu, ponieważ początkowo zapomniałem „it ()”.

JamesEggers
źródło

Odpowiedzi:

21

Zmień obiekt odpowiedzi:

var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        this.viewName = view;
        this.data = viewData;
    }
};

I to zadziała.

Linus Thiel
źródło
4
To jest testowanie jednostkowe modułu obsługi żądań, a nie trasy.
Jason Sebring
43

Jak inni zalecali w komentarzach, wygląda na to, że kanonicznym sposobem testowania kontrolerów Express jest supertest .

Przykładowy test może wyglądać następująco:

describe('GET /users', function(){
  it('respond with json', function(done){
    request(app)
      .get('/users')
      .set('Accept', 'application/json')
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        done()
      });
  })
});

Upside: możesz przetestować cały swój stack za jednym razem.

Wada: czuje się i działa trochę jak testowanie integracyjne.

Rich Apodaca
źródło
1
Podoba mi się to, ale czy istnieje sposób na potwierdzenie viewName (jak w pierwotnym pytaniu) - czy też musielibyśmy potwierdzić treść odpowiedzi?
Alex
21
Zgadzam się z twoją wadą, to nie jest testowanie jednostkowe. Zależy to od integracji wszystkich jednostek w celu przetestowania adresów URL aplikacji.
Luke H
10
Myślę, że legalne jest stwierdzenie, że „trasa” jest tak naprawdę integrationi być może ścieżki testowe należy pozostawić testom integracyjnym. Mam na myśli to, że funkcjonalność tras dopasowujących się do ich zdefiniowanych wywołań zwrotnych jest prawdopodobnie już przetestowana przez express.js; jakakolwiek wewnętrzna logika uzyskiwania końcowego wyniku trasy, powinna być w idealnym przypadku modularyzowana poza nią, a te moduły powinny być testowane jednostkowo. Ich interakcję, czyli trasę, należy przetestować pod kątem integracji. Zgodziłbyś się?
Aditya MP
1
To testowanie od początku do końca. Bez wątpienia.
kgpdeveloper,
25

Doszedłem do wniosku, że jedynym sposobem naprawdę testowania jednostkowego aplikacji ekspresowych jest zachowanie dużej separacji między programami obsługi żądań a podstawową logiką.

W związku z tym logika aplikacji powinna znajdować się w oddzielnych modułach, które można requireprzetestować d i jednostkowo, i mieć minimalną zależność od klas Express Request i Response jako takich.

Następnie w programach obsługi żądań należy wywołać odpowiednie metody podstawowych klas logiki.

Podam przykład, gdy zakończę restrukturyzację mojej obecnej aplikacji!

Chyba coś takiego ? (Zapraszam do rozwidlenia istoty lub komentarza, wciąż to badam).

Edytować

Oto mały przykład, inline. Zobacz sedno, aby uzyskać bardziej szczegółowy przykład.

/// usercontroller.js
var UserController = {
   _database: null,
   setDatabase: function(db) { this._database = db; },

   findUserByEmail: function(email, callback) {
       this._database.collection('usercollection').findOne({ email: email }, callback);
   }
};

module.exports = UserController;

/// routes.js

/* GET user by email */
router.get('/:email', function(req, res) {
    var UserController = require('./usercontroller');
    UserController.setDB(databaseHandleFromSomewhere);
    UserController.findUserByEmail(req.params.email, function(err, result) {
        if (err) throw err;
        res.json(result);
    });
});
Luke H.
źródło
3
Moim zdaniem to najlepszy wzór do wykorzystania. Wiele platform internetowych w różnych językach używa wzorca kontrolera do oddzielenia logiki biznesowej od rzeczywistej funkcji tworzenia odpowiedzi http. W ten sposób możesz po prostu przetestować logikę, a nie cały proces odpowiedzi http, co jest czymś, co programiści frameworka powinni przetestować samodzielnie. Inne rzeczy, które można przetestować w tym wzorcu, to proste oprogramowanie pośredniczące, niektóre funkcje walidacji i inne usługi biznesowe. Testowanie łączności DB to jednak zupełnie inny rodzaj testów
OzzyTheGiant
1
Rzeczywiście, wiele odpowiedzi tutaj dotyczy tak naprawdę testów integracyjnych / funkcjonalnych.
Luke H
To jest właściwa odpowiedź. Powinieneś skupić się na testowaniu swojej logiki, a nie Express.
esmiralha
19

Najłatwiejszym sposobem przetestowania HTTP za pomocą express jest kradzież pomocnika http TJ-a

Ja osobiście używać swojego pomocnika

it("should do something", function (done) {
    request(app())
    .get('/session/new')
    .expect('GET', done)
})

Jeśli chcesz dokładnie przetestować obiekt tras, przekaż poprawne makiety

describe("Default Route", function(){
    it("should provide the a title and the index view name", function(done){
        routes.index({}, {
            render: function (viewName) {
                viewName.should.equal("index")
                done()
            }
        })
    })
})
Raynos
źródło
5
czy możesz naprawić link „pomocnik”?
Nicholas Murray
16
Wydaje się, że bardziej aktualnym podejściem do testów jednostkowych HTTP jest zastosowanie supertestu firmy Visionmedia. Wygląda również na to, że pomocnik http TJ przekształcił się w supertest.
Akseli Palén
2
supertest na githubie można znaleźć tutaj
Brandon
@Raynos czy mógłbyś wyjaśnić, w jaki sposób otrzymujesz żądanie i aplikację w swoim przykładzie?
jmcollin92
9
Niestety jest to raczej testowanie integracyjne niż jednostkowe.
Luke H
8

jeśli testy jednostkowe z express 4, zwróć uwagę na ten przykład od gjohnson :

var express = require('express');
var request = require('supertest');
var app = express();
var router = express.Router();
router.get('/user', function(req, res){
  res.send(200, { name: 'tobi' });
});
app.use(router);
request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end(function(err, res){
    if (err) throw err;
  });
ErichBSchulz
źródło
1

Też się nad tym zastanawiałem, ale specjalnie dla testów jednostkowych, a nie testów integracyjnych. To jest to, co teraz robię

test('/api base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/api');
});


test('Subrouters loaded', function onTest(t) {
  t.plan(1);

  var router = routerObj.router;

  t.equals(router.stack.length, 5);
});

Gdzie routerObj jest po prostu {router: expressRouter, path: '/api'}. Następnie ładuję podroutery za pomocą, var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));a aplikacja ekspresowa wywołuje funkcję init, przyjmując jako parametr router ekspresowy. Następnie initRouter wywołuje router.use(loginRouterInfo.path, loginRouterInfo.router);zamontowanie routera podrzędnego.

Podrouter można przetestować za pomocą:

var test = require('tape');
var routerInit = require('../login');
var express = require('express');
var routerObj = routerInit(express.Router());

test('/login base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/login');
});


test('GET /', function onTest(t) {
  t.plan(2);

  var route = routerObj.router.stack[0].route;

  var routeGetMethod = route.methods.get;
  t.equals(routeGetMethod, true);

  var routePath = route.path;
  t.equals(routePath, '/');
});
Marcus Rådell
źródło
3
To wygląda naprawdę interesująco. Czy masz więcej przykładów brakujących elementów, aby pokazać, jak to wszystko do siebie pasuje?
cjbarth
1

Aby uzyskać testowanie jednostkowe zamiast testowania integracji, wyśmiałem obiekt odpowiedzi modułu obsługi żądań.

/* app.js */
import endpointHandler from './endpointHandler';
// ...
app.post('/endpoint', endpointHandler);
// ...

/* endpointHandler.js */
const endpointHandler = (req, res) => {
  try {
    const { username, location } = req.body;

    if (!(username && location)) {
      throw ({ status: 400, message: 'Missing parameters' });
    }

    res.status(200).json({
      location,
      user,
      message: 'Thanks for sharing your location with me.',
    });
  } catch (error) {
    console.error(error);
    res.status(error.status).send(error.message);
  }
};

export default endpointHandler;

/* response.mock.js */
import { EventEmitter } from 'events';

class Response extends EventEmitter {
  private resStatus;

  json(response, status) {
    this.send(response, status);
  }

  send(response, status) {
    this.emit('response', {
      response,
      status: this.resStatus || status,
    });
  }

  status(status) {
    this.resStatus = status;
    return this;
  }
}

export default Response;

/* endpointHandler.test.js */
import Response from './response.mock';
import endpointHandler from './endpointHander';

describe('endpoint handler test suite', () => {
  it('should fail on empty body', (done) => {
    const res = new Response();

    res.on('response', (response) => {
      expect(response.status).toBe(400);
      done();
    });

    endpointHandler({ body: {} }, res);
  });
});

Następnie, aby przeprowadzić testy integracji, możesz mockować swój endpointHandler i wywołać punkt końcowy za pomocą supertestu .

fxlemire
źródło
0

W moim przypadku jedyne, co chciałem sprawdzić, to czy został wezwany właściwy handler. Chciałem użyć supertestu, aby zmniejszyć prostotę wysyłania żądań do oprogramowania pośredniczącego routingu. Używam Typescript a i jest to rozwiązanie, które zadziałało

// ProductController.ts

import { Request, Response } from "express";

class ProductController {
  getAll(req: Request, res: Response): void {
    console.log("this has not been implemented yet");
  }
}
export default ProductController

Trasy

// routes.ts
import ProductController  from "./ProductController"

const app = express();
const productController = new ProductController();
app.get("/product", productController.getAll);

Testy

// routes.test.ts

import request from "supertest";
import { Request, Response } from "express";

const mockGetAll = jest
  .fn()
  .mockImplementation((req: Request, res: Response) => {
    res.send({ value: "Hello visitor from the future" });
  });

jest.doMock("./ProductController", () => {
  return jest.fn().mockImplementation(() => {
    return {
      getAll: mockGetAll,

    };
  });
});

import app from "./routes";

describe("Routes", () => {
  beforeEach(() => {
    mockGetAll.mockImplementation((req: Request, res: Response) => {
      res.send({ value: "You can also change the implementation" });
    });
  });

  it("GET /product integration test", async () => {
    const result = await request(app).get("/product");

    expect(mockGetAll).toHaveBeenCalledTimes(1);

  });



  it("GET an undefined route should return status 404", async () => {
    const response = await request(app).get("/random");
    expect(response.status).toBe(404);
  });
});

Miałem pewne problemy, aby kpina zadziałała. ale użycie jest.doMock i kolejność, którą widzisz w przykładzie, sprawia, że ​​działa.

Alvaro
źródło