¿Cómo prueba una unidad las rutas con Express?

99

Estoy en el proceso de aprender Node.js y he estado jugando con Express . Realmente me gusta el marco; sin embargo, tengo problemas para averiguar cómo escribir una prueba de unidad / integración para una ruta.

Poder realizar pruebas unitarias de módulos simples es fácil y lo he estado haciendo con Mocha ; sin embargo, mis pruebas unitarias con Express fallan porque el objeto de respuesta que estoy pasando no retiene los valores.

Función de ruta bajo prueba (rutas / index.js):

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

Módulo de prueba unitaria:

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

    });
});

Cuando ejecuto esto, falla por "Error: fugas globales detectadas: viewName, datos".

  1. ¿Dónde voy mal para que esto funcione?

  2. ¿Existe una mejor manera de realizar una prueba unitaria de mi código en este nivel?

Actualización 1. Fragmento de código corregido ya que inicialmente olvidé "it ()".

JamesEggers
fuente

Respuestas:

21

Cambia tu objeto de respuesta:

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

Y funcionará.

Linus Thiel
fuente
2
Esta es una unidad de prueba de un controlador de solicitudes, no una ruta.
Jason Sebring
43

Como otros han recomendado en los comentarios, parece que la forma canónica de probar los controladores Express es mediante supertest .

Una prueba de ejemplo podría verse así:

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

Ventaja: puedes probar toda tu pila de una sola vez.

Desventaja: se siente y actúa un poco como una prueba de integración.

Rico Apodaca
fuente
1
Me gusta esto, pero ¿hay alguna manera de afirmar el viewName (como en la pregunta original), o tendríamos que afirmar sobre el contenido de la respuesta?
Alex
19
Estoy de acuerdo con su inconveniente, esto no es una prueba unitaria. Esto se basa en la integración de todas sus unidades para probar las URL de su aplicación.
Luke H
10
Creo que es legal decir que una "ruta" es realmente una integration, y quizás las rutas de prueba deberían dejarse para las pruebas de integración. Quiero decir, la funcionalidad de las rutas que coinciden con sus devoluciones de llamada definidas presumiblemente ya está probada por express.js; cualquier lógica interna para obtener el resultado final de una ruta, idealmente debería modularizarse fuera de ella, y esos módulos deberían probarse unitariamente. Su interacción, es decir, la ruta, debe probarse por integración. ¿Estarías de acuerdo?
Aditya MP
1
Es una prueba de extremo a extremo. Sin duda.
kgpdeveloper
23

Llegué a la conclusión de que la única forma de realizar pruebas unitarias de las aplicaciones express es mantener mucha separación entre los controladores de solicitudes y la lógica central.

Por lo tanto, la lógica de su aplicación debe estar en módulos separados que se puedan requireprobar por unidad y tener una dependencia mínima de las clases Express Request y Response como tales.

Luego, en los controladores de solicitud, debe llamar a los métodos apropiados de sus clases lógicas centrales.

¡Daré un ejemplo una vez que haya terminado de reestructurar mi aplicación actual!

¿Supongo algo como esto? (Siéntase libre de bifurcar la esencia o comentar, todavía estoy explorando esto).

Editar

Aquí hay un pequeño ejemplo, en línea. Consulte la esencia para obtener un ejemplo más detallado.

/// 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
fuente
3
En mi opinión, este es el mejor patrón a utilizar. Muchos marcos web en todos los idiomas utilizan el patrón de controlador para separar la lógica empresarial de la funcionalidad de formación de respuesta http real. De esta manera, puede probar la lógica y no todo el proceso de respuesta http, que es algo que los desarrolladores del marco deberían probar por sí mismos. Otras cosas que podrían probarse en este patrón son middlewares simples, algunas funciones de validación y otros servicios comerciales. Sin embargo
OzzyTheGiant
1
De hecho, muchas de las respuestas aquí tienen realmente que ver con la integración / pruebas funcionales.
Luke H
19

La forma más fácil de probar HTTP con express es robar el ayudante http de TJ

Yo personalmente uso a su ayudante

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

Si desea probar específicamente su objeto de rutas, pase los simulacros correctos

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
fuente
5
¿Podrías arreglar el enlace 'ayudante'?
Nicholas Murray
16
Parece que un enfoque más actualizado para las pruebas unitarias HTTP es usar supertest de Visionmedia. También parece que el ayudante http de TJ ha evolucionado hasta convertirse en supertest.
Akseli Palén
2
supertest en github se puede encontrar aquí
Brandon
@Raynos, ¿podría explicar cómo obtiene una solicitud y una aplicación en su ejemplo?
jmcollin92
9
Lamentablemente, se trata de pruebas de integración en lugar de pruebas unitarias.
Luke H
8

si prueba unitaria con express 4, tenga en cuenta este ejemplo 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
fuente
1

También me preguntaba esto, pero específicamente para pruebas unitarias y no pruebas de integración. Esto es lo que estoy haciendo ahora mismo

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

Donde el routerObj es justo {router: expressRouter, path: '/api'}. Luego cargo en subrouters con var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));y luego la aplicación express llama a una función init que toma el enrutador express como parámetro. El initRouter luego llama router.use(loginRouterInfo.path, loginRouterInfo.router);para montar el subrouter.

El subrouter se puede probar con:

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
fuente
3
Esto parece realmente interesante. ¿Tiene más ejemplos de las piezas que faltan para mostrar cómo encaja todo esto?
cjbarth
1

Para lograr pruebas unitarias en lugar de pruebas de integración, me burlé del objeto de respuesta del controlador de solicitudes.

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

Luego, para lograr las pruebas de integración, puede simular su endpointHandler y llamar al endpoint con supertest .

fxlemire
fuente
0

En mi caso, lo único que quería probar es si se ha llamado al manejador derecho. Quería usar supertest para aprovechar la simplicidad de realizar las solicitudes al middleware de enrutamiento. Estoy usando TypeScript ay esta es la solución que funcionó para mí

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

Las rutas

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

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

Los exámenes

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

Tuve algunos problemas para que la burla funcionara. pero usar jest.doMock y el orden específico que ve en el ejemplo lo hace funcionar.

Álvaro
fuente