In che modo un test di unità viene instradato con Express?


99

Sto imparando a utilizzare Node.js e sto giocando con Express . Mi piace molto il framework; tuttavia, ho problemi a capire come scrivere un test di unità / integrazione per un percorso.

Essere in grado di testare unità semplici moduli è facile e l'ho fatto con Mocha ; tuttavia, i miei unit test con Express hanno esito negativo poiché l'oggetto risposta che sto passando non conserva i valori.

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

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

Modulo di test unitario:

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 lo eseguo, non riesce per "Errore: rilevate perdite globali: viewName, dati".

  1. Dove sto sbagliando in modo che possa farlo funzionare?

  2. C'è un modo migliore per me di testare il mio codice a questo livello?

Aggiornamento 1. Corretto frammento di codice poiché inizialmente avevo dimenticato "it ()".

Risposte:


21

Cambia il tuo oggetto risposta:

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

E funzionerà.


2
Questo è un test unitario di un gestore di richieste, non una route.
Jason Sebring

43

Come altri hanno consigliato nei commenti, sembra che il modo canonico per testare i controller Express sia tramite supertest .

Un test di esempio potrebbe essere simile a questo:

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: puoi testare l'intero stack in una volta.

Lato negativo: si sente e si comporta un po 'come un test di integrazione.


1
Mi piace, ma esiste un modo per affermare viewName (come nella domanda originale) - o dovremmo affermare sul contenuto della risposta?
Alex

19
Sono d'accordo con il tuo lato negativo, questo non è test unitario. Ciò si basa sull'integrazione di tutte le unità per testare gli URL dell'applicazione.
Luke H

10
Penso che sia legale dire che un "percorso" è davvero un integration, e forse i test delle rotte dovrebbero essere lasciati ai test di integrazione. Voglio dire, la funzionalità delle rotte che corrispondono ai loro callback definiti è presumibilmente già testata da express.js; qualsiasi logica interna per ottenere il risultato finale di un percorso, dovrebbe idealmente essere modularizzata al di fuori di essa, e quei moduli dovrebbero essere unit test. La loro interazione, cioè il percorso, dovrebbe essere testata per l'integrazione. Saresti d'accordo?
Aditya MP

1
È un test end-to-end. Nessun dubbio.
kgpdeveloper

23

Sono giunto alla conclusione che l'unico modo per testare veramente le applicazioni express è mantenere molta separazione tra i gestori delle richieste e la logica di base.

Pertanto, la logica dell'applicazione dovrebbe trovarsi in moduli separati che possono essere requiretestati su unità e avere una dipendenza minima dalle classi di richiesta espressa e risposta in quanto tali.

Quindi nei gestori delle richieste è necessario chiamare i metodi appropriati delle classi logiche principali.

Farò un esempio una volta terminata la ristrutturazione della mia attuale app!

Immagino che qualcosa del genere ? (Sentiti libero di sborsare il succo o commentare, sto ancora esplorando questo).

modificare

Ecco un piccolo esempio, in linea. Vedi l'essenza per un esempio più dettagliato.

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

3
Secondo me, questo è il modello migliore da usare. Molti framework Web in diversi linguaggi utilizzano il pattern controller per separare la logica di business dall'effettiva funzionalità di formazione della risposta http. In questo modo, puoi semplicemente testare la logica e non l'intero processo di risposta http, che è qualcosa che gli sviluppatori del framework dovrebbero testare da soli. Altre cose che potrebbero essere testate in questo modello sono semplici middleware, alcune funzioni di convalida e altri servizi aziendali. Il test della connettività DB è un tipo di test completamente diverso
OzzyTheGiant il

1
In effetti, molte delle risposte qui hanno davvero a che fare con l'integrazione / test funzionale.
Luke H

19

Il modo più semplice per testare HTTP con express è rubare l'helper http di TJ

Io personalmente uso il suo aiutante

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

Se vuoi testare specificamente il tuo oggetto rotte, passa i mock corretti

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

5
potresti correggere il link "helper"?
Nicholas Murray

16
Sembra che l'approccio più aggiornato allo unit test HTTP sia quello di utilizzare supertest di Visionmedia. Sembra anche che l'helper http di TJ si sia evoluto in supertest.
Akseli Palén

2
supertest su GitHub può essere trovato qui
Brandon

@ Raynos potresti spiegare come si ottiene una richiesta e un'app nel tuo esempio?
jmcollin92

9
Purtroppo questo è un test di integrazione piuttosto che un test di unità.
Luke H

8

se unit test con express 4 nota questo esempio di 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;
  });

1

Mi chiedevo anche questo, ma specificamente per i test unitari e non per i test di integrazione. Questo è quello che sto facendo adesso

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

Dove il routerObj è solo {router: expressRouter, path: '/api'}. Quindi carico i subrouter con var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));e quindi l'app express chiama una funzione di inizializzazione che prende il router express come parametro. L'initRouter chiama quindi router.use(loginRouterInfo.path, loginRouterInfo.router);per montare il subrouter.

Il subrouter può essere testato 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, '/');
});

3
Sembra davvero interessante. Hai altri esempi dei pezzi mancanti per mostrare come tutto questo combacia?
cjbarth

1

Per ottenere test unitari anziché test di integrazione, ho deriso l'oggetto risposta del gestore delle richieste.

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

Quindi, per ottenere il test di integrazione, puoi deridere il tuo endpointHandler e chiamare l'endpoint con supertest .


0

Nel mio caso l'unico che volevo verificare è se è stato chiamato il gestore destro. Volevo utilizzare supertest per sfruttare la semplicità di effettuare le richieste al middleware di routing. Sto usando Typescript a e questa è la soluzione che ha funzionato per me

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

I percorsi

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

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

I test

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

Ho avuto dei problemi per far funzionare la presa in giro. ma usare jest.doMock e l'ordine specifico che vedi nell'esempio lo fa funzionare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.