Como uma unidade testa as rotas com o Express?

99

Estou aprendendo Node.js e estou brincando com o Express . Gosto muito da estrutura; no entanto, estou tendo problemas para descobrir como escrever um teste de unidade / integração para uma rota.

Ser capaz de testar módulos simples é fácil e tenho feito isso com o Mocha ; no entanto, meus testes de unidade com Express falham, pois o objeto de resposta que estou passando não retém os valores.

Função de rota em teste (routes / index.js):

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

Módulo de Teste de Unidade:

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

    });
});

Quando eu executo isso, ele falha para "Erro: vazamentos globais detectados: viewName, dados".

  1. Onde estou errando para que eu possa fazer isso funcionar?

  2. Existe uma maneira melhor para eu testar a unidade do meu código neste nível?

Atualização 1. Trecho de código corrigido, pois inicialmente esqueci "it ()".

JamesEggers
fonte

Respostas:

21

Altere seu objeto de resposta:

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

E vai funcionar.

Linus Thiel
fonte
2
Este é um teste de unidade de um manipulador de solicitação, não uma rota.
Jason Sebring
43

Como outros recomendaram em comentários, parece que a maneira canônica de testar os controladores Express é por meio do superteste .

Um exemplo de teste pode ser assim:

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

Positivo: você pode testar toda a sua pilha de uma vez.

Desvantagem: parece e age um pouco como um teste de integração.

Rich Apodaca
fonte
1
Eu gosto disso, mas há uma maneira de afirmar o viewName (como na pergunta original) - ou teríamos que fazer a declaração sobre o conteúdo da resposta?
Alex
19
Eu concordo com sua desvantagem, este não é um teste de unidade. Isso depende da integração de todas as suas unidades para testar os urls de seu aplicativo.
Luke H
10
Acho que é legal dizer que uma "rota" é realmente uma integration, e talvez as rotas de teste devam ser deixadas para os testes de integração. Quer dizer, a funcionalidade das rotas correspondentes aos retornos de chamada definidos já foi testada pelo express.js; qualquer lógica interna para obter o resultado final de uma rota, idealmente, deve ser modularizada fora dela e esses módulos devem ser testados na unidade. Sua interação, ou seja, a rota, deve ser testada para integração. Você concordaria?
Aditya MP
1
É um teste de ponta a ponta. Sem dúvida.
kgpdeveloper
23

Cheguei à conclusão de que a única maneira de realmente testar a unidade de aplicativos expressos é manter uma grande separação entre os manipuladores de solicitação e sua lógica central.

Portanto, a lógica de seu aplicativo deve estar em módulos separados que podem ser requiretestados em unidades e ter uma dependência mínima das classes Express Request e Response como tal.

Em seguida, nos manipuladores de solicitação, você precisa chamar os métodos apropriados de suas classes lógicas centrais.

Vou colocar um exemplo assim que terminar de reestruturar meu aplicativo atual!

Eu acho que algo assim ? (Sinta-se à vontade para desvendar a essência ou comentar, ainda estou explorando isso).

Editar

Aqui está um pequeno exemplo, embutido. Veja a essência para um exemplo mais detalhado.

/// 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
fonte
3
Na minha opinião, este é o melhor padrão a ser usado. Muitas estruturas da web em linguagens usam o padrão de controlador para separar a lógica de negócios da funcionalidade real de formação de resposta http. Dessa forma, você pode apenas testar a lógica e não todo o processo de resposta http, que é algo que os desenvolvedores do framework devem testar por conta própria. Outras coisas que podem ser testadas neste padrão são middlewares simples, algumas funções de validação e outros serviços de negócios. O teste de conectividade DB é um tipo totalmente diferente de teste
OzzyTheGiant
1
Na verdade, muitas das respostas aqui têm realmente a ver com testes de integração / funcionais.
Luke H
19

A maneira mais fácil de testar HTTP com express é roubar o assistente de http do TJ

Eu pessoalmente uso o ajudante dele

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

Se você quiser testar especificamente seu objeto de rotas, passe simulações corretas

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
fonte
5
você poderia corrigir o link 'ajudante'?
Nicholas Murray
16
Parece que uma abordagem mais atualizada para o teste de unidade HTTP é usar o supertest da Visionmedia. Parece também que o auxiliar http do TJ evoluiu para superteste.
Akseli Palén
2
superteste no github pode ser encontrado aqui
Brandon
@Raynos você poderia explicar como obtém uma solicitação e um aplicativo em seu exemplo?
jmcollin92
9
Infelizmente, este é um teste de integração em vez de um teste de unidade.
Luke H
8

se o teste de unidade com express 4 observe este exemplo 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
fonte
1

Eu também estava me perguntando isso, mas especificamente para testes de unidade e não testes de integração. Isso é o que estou fazendo agora,

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

Onde o routerObj fica justo {router: expressRouter, path: '/api'}. Em seguida, carrego os sub-roteadores com var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));e o aplicativo expresso chama uma função init tomando o roteador expresso como parâmetro. O initRouter então chama router.use(loginRouterInfo.path, loginRouterInfo.router);para montar o subrouter.

O sub-roteador pode ser testado com:

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
fonte
3
Isso parece muito interessante. Você tem mais exemplos das peças que faltam para mostrar como tudo isso se encaixa?
cjbarth
1

Para realizar o teste de unidade em vez do teste de integração, eu simulei o objeto de resposta do manipulador de solicitação.

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

Então, para realizar o teste de integração, você pode simular seu endpointHandler e chamar o endpoint com supertest .

fxlemire
fonte
0

No meu caso, só queria testar se o manipulador direito foi chamado. Eu queria usar o supertest para lavar a simplicidade de fazer as solicitações ao middleware de roteamento. Estou usando o Typescript a e esta é a solução que funcionou para mim

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

As rotas

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

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

Os testes

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

Tive alguns problemas para fazer a zombaria funcionar. mas usar jest.doMock e a ordem específica que você vê no exemplo faz com que funcione.

Alvaro
fonte