Comment une unité teste-t-elle les routes avec Express?

99

Je suis en train d'apprendre Node.js et j'ai joué avec Express . J'aime vraiment le framework; cependant, j'ai du mal à comprendre comment écrire un test unitaire / d'intégration pour une route.

Être capable de tester des modules simples est facile et je l'ai fait avec Mocha ; cependant, mes tests unitaires avec Express échouent car l'objet de réponse que je transmets ne conserve pas les valeurs.

Route-Function Under Test (routes / index.js):

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

Module de test unitaire:

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");
        });

    });
});

Quand j'exécute ceci, cela échoue pour "Erreur: fuites globales détectées: viewName, data".

  1. Où est-ce que je vais mal pour que je puisse faire fonctionner cela?

  2. Existe-t-il un meilleur moyen pour moi de tester mon code unitaire à ce niveau?

Mise à jour 1. Extrait de code corrigé depuis que j'ai initialement oublié "it ()".

JamesEggers
la source

Réponses:

21

Modifiez votre objet de réponse:

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

Et cela fonctionnera.

Linus Thiel
la source
2
Il s'agit d'un test unitaire d'un gestionnaire de requêtes, pas d'une route.
Jason Sebring
43

Comme d'autres l'ont recommandé dans les commentaires, il semble que la manière canonique de tester les contrôleurs Express consiste à effectuer un super test .

Un exemple de test pourrait ressembler à ceci:

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()
      });
  })
});

À l'inverse: vous pouvez tester l'intégralité de votre stack en une seule fois.

Inconvénient: cela ressemble et agit un peu comme des tests d'intégration.

Rich Apodaca
la source
1
J'aime cela, mais y a-t-il un moyen d'affirmer le nom de vue (comme dans la question d'origine) - ou devrions-nous affirmer sur le contenu de la réponse?
Alex
20
Je suis d'accord avec votre inconvénient, ce n'est pas un test unitaire. Cela repose sur l'intégration de toutes vos unités pour tester les URL de votre application.
Luke H
10
Je pense qu'il est légal de dire qu'une "route" est vraiment une integration, et peut-être que les routes de test devraient être laissées aux tests d'intégration. Je veux dire, la fonctionnalité des routes correspondant à leurs rappels définis est probablement déjà testée par express.js; toute logique interne pour obtenir le résultat final d'une route devrait idéalement être modulaire à l'extérieur de celle-ci, et ces modules devraient être testés à l'unité. Leur interaction, c'est-à-dire l'itinéraire, doit être testée en intégration. Accepteriez-vous?
Aditya MP
1
Il s'agit de tests de bout en bout. Sans aucun doute.
kgpdeveloper
23

J'en suis venu à la conclusion que la seule façon de vraiment tester les applications expresses unitaire est de maintenir une grande séparation entre les gestionnaires de requêtes et votre logique de base.

Ainsi, la logique de votre application doit se trouver dans des modules séparés qui peuvent être requiretestés d et unitaire, et dépendre au minimum des classes Express Request et Response en tant que telles.

Ensuite, dans les gestionnaires de requêtes, vous devez appeler les méthodes appropriées de vos classes logiques de base.

Je donnerai un exemple une fois que j'aurai fini de restructurer mon application actuelle!

Je suppose que quelque chose comme ça? (N'hésitez pas à donner l'essentiel ou à commenter, j'explore toujours cela).

Éditer

Voici un petit exemple, en ligne. Voir l'essentiel pour un exemple plus détaillé.

/// 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);
    });
});
Luc H
la source
3
À mon avis, c'est le meilleur modèle à utiliser. De nombreux frameworks Web à travers les langues utilisent le modèle de contrôleur pour séparer la logique métier de la fonctionnalité de formation de réponse http réelle. De cette façon, vous pouvez simplement tester la logique et non tout le processus de réponse http, ce que les développeurs du framework devraient tester eux-mêmes. D'autres choses qui pourraient être testées dans ce modèle sont des middlewares simples, certaines fonctions de validation et d'autres services métier. Les tests de connectivité à la base de données sont cependant un type de test complètement différent
OzzyTheGiant
1
En effet, beaucoup de réponses ici sont vraiment liées à l'intégration / aux tests fonctionnels.
Luke H
19

Le moyen le plus simple de tester HTTP avec express est de voler l'assistant http de TJ

J'utilise personnellement son aide

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

Si vous souhaitez tester spécifiquement votre objet routes, passez des simulations correctes

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
la source
5
pourriez-vous réparer le lien «helper»?
Nicholas Murray
16
Il semble qu'une approche plus à jour des tests unitaires HTTP consiste à utiliser supertest de Visionmedia. Il semble également que l'assistant http de TJ ait évolué vers le supertest.
Akseli Palén
2
supertest sur github peut être trouvé ici
Brandon
@Raynos pourriez-vous expliquer comment vous obtenez une demande et une application dans votre exemple?
jmcollin92
9
Malheureusement, il s'agit de tests d'intégration plutôt que de tests unitaires.
Luke H
8

si les tests unitaires avec express 4 notez cet exemple de 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
la source
1

Je me posais la question aussi, mais spécifiquement pour les tests unitaires et non pour les tests d'intégration. C'est ce que je fais en ce moment

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);
});

Où le routerObj est juste {router: expressRouter, path: '/api'}. Je charge ensuite des sous-routeurs avec var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));, puis l'application express appelle une fonction init prenant le routeur express comme paramètre. L'initRouter appelle ensuite router.use(loginRouterInfo.path, loginRouterInfo.router);pour monter le sous-routeur.

Le sous-routeur peut être testé avec:

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
la source
3
Cela semble vraiment intéressant. Avez-vous plus d'exemples des pièces manquantes pour montrer comment tout cela s'accorde?
cjbarth
1

Pour réaliser des tests unitaires au lieu des tests d'intégration, je me suis moqué de l'objet de réponse du gestionnaire de requêtes.

/* 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);
  });
});

Ensuite, pour réaliser les tests d'intégration, vous pouvez simuler votre endpointHandler et appeler le point de terminaison avec supertest .

fxlemire
la source
0

Dans mon cas, le seul que je voulais tester est si le bon gestionnaire a été appelé. Je voulais utiliser supertest pour profiter de la simplicité de faire des requêtes au middleware de routage. J'utilise Typescript a et c'est la solution qui a fonctionné pour moi

// 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

Les routes

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

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

Les tests

// 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);
  });
});

J'ai eu quelques problèmes pour que la moquerie fonctionne. mais en utilisant jest.doMock et l'ordre spécifique que vous voyez dans l'exemple le fait fonctionner.

Alvaro
la source