Wie testet man Routen mit Express?

99

Ich bin gerade dabei, Node.js zu lernen und habe mit Express herumgespielt . Ich mag das Framework wirklich, aber ich habe Probleme herauszufinden, wie man einen Einheits- / Integrationstest für eine Route schreibt.

Es ist einfach, einfache Module zu testen, und das mit Mocha . Meine Komponententests mit Express schlagen jedoch fehl, da das von mir übergebene Antwortobjekt die Werte nicht beibehält.

Zu testende Routenfunktion (route / index.js):

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

Unit Test Modul:

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

    });
});

Wenn ich dies ausführe, schlägt es fehl für "Fehler: Globale Lecks erkannt: Ansichtsname, Daten".

  1. Wo mache ich etwas falsch, damit das funktioniert?

  2. Gibt es eine bessere Möglichkeit für mich, meinen Code auf dieser Ebene zu testen?

Update 1. Code-Snippet korrigiert, da ich "it ()" ursprünglich vergessen habe.

JamesEggers
quelle

Antworten:

21

Ändern Sie Ihr Antwortobjekt:

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

Und es wird funktionieren.

Linus Thiel
quelle
2
Dies ist ein Komponententest für einen Anforderungshandler, keine Route.
Jason Sebring
43

Wie andere in Kommentaren empfohlen haben, sieht es so aus, als ob der kanonische Weg zum Testen von Express-Controllern der Supertest ist .

Ein Beispieltest könnte folgendermaßen aussehen:

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

Vorteil: Sie können Ihren gesamten Stack auf einmal testen.

Nachteil: Es fühlt sich an und verhält sich ein bisschen wie Integrationstests.

Reiches Apodaca
quelle
1
Ich mag das, aber gibt es eine Möglichkeit, den viewName (wie in der ursprünglichen Frage) zu behaupten - oder müssten wir den Inhalt der Antwort bestätigen?
Alex
19
Ich stimme Ihrem Nachteil zu, dies ist kein Unit-Test. Dies hängt von der Integration aller Ihrer Einheiten ab, um die URLs Ihrer Anwendung zu testen.
Luke H
10
Ich denke, es ist legal zu sagen, dass eine "Route" wirklich eine ist integration, und vielleicht sollten Testrouten Integrationstests überlassen werden. Ich meine, die Funktionalität von Routen, die mit ihren definierten Rückrufen übereinstimmen, wird vermutlich bereits von express.js getestet. Jede interne Logik zum Erhalten des Endergebnisses einer Route sollte idealerweise außerhalb dieser Route modularisiert werden, und diese Module sollten Unit-getestet werden. Ihre Interaktion, dh die Route, sollte integrationsgeprüft werden. Würdest du zustimmen?
Aditya MP
1
Es ist ein End-to-End-Test. Kein Zweifel.
kgpdeveloper
23

Ich bin zu dem Schluss gekommen, dass der einzige Weg, um Express-Anwendungen wirklich zu testen, darin besteht, eine große Trennung zwischen den Anforderungshandlern und Ihrer Kernlogik aufrechtzuerhalten.

Daher sollte sich Ihre Anwendungslogik in separaten Modulen befinden, die requiregetestet und einheitlich getestet werden können, und eine minimale Abhängigkeit von den Express-Anforderungs- und Antwortklassen als solchen aufweisen.

Dann müssen Sie in den Anforderungshandlern die entsprechenden Methoden Ihrer Kernlogikklassen aufrufen.

Ich werde ein Beispiel aufstellen, sobald ich meine aktuelle App umstrukturiert habe!

Ich denke so etwas ? (Fühlen Sie sich frei, das Wesentliche oder den Kommentar zu teilen, ich untersuche dies immer noch).

Bearbeiten

Hier ist ein kleines Beispiel, Inline. Ein detaillierteres Beispiel finden Sie in der Übersicht .

/// 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.
quelle
3
Meiner Meinung nach ist dies das beste Muster. Viele sprachübergreifende Web-Frameworks verwenden das Controller-Muster, um die Geschäftslogik von der eigentlichen Funktionalität zur Bildung von http-Antworten zu trennen. Auf diese Weise können Sie nur die Logik und nicht den gesamten http-Antwortprozess testen. Dies sollten die Entwickler des Frameworks selbst testen. Andere Dinge, die in diesem Muster getestet werden könnten, sind einfache Middlewares, einige Validierungsfunktionen und andere Geschäftsdienste. DB-Konnektivitätstests sind jedoch eine ganz andere Art von Tests
OzzyTheGiant
1
In der Tat haben viele der Antworten hier wirklich mit Integration / Funktionstests zu tun.
Luke H
19

Der einfachste Weg, HTTP mit Express zu testen, besteht darin, den http-Helfer von TJ zu stehlen

Ich persönlich benutze seinen Helfer

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

Wenn Sie Ihr Routenobjekt speziell testen möchten, übergeben Sie die richtigen Mocks

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
quelle
5
Könntest du den 'Helfer'-Link reparieren?
Nicholas Murray
16
Es scheint, dass ein aktuellerer Ansatz für HTTP-Unit-Tests darin besteht, den Supertest von Visionmedia zu verwenden. Es scheint auch, dass sich der http-Helfer von TJ zum Supertest entwickelt hat.
Akseli Palén
2
Supertest auf Github finden Sie hier
Brandon
@ Raynos Könnten Sie in Ihrem Beispiel erklären, wie Sie eine Anfrage und eine App erhalten?
jmcollin92
9
Leider handelt es sich hierbei eher um Integrationstests als um Unit-Tests.
Luke H
8

Wenn Unit-Tests mit Express 4 durchgeführt werden, beachten Sie dieses Beispiel von 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
quelle
1

Ich habe mich auch gefragt, aber speziell für Unit-Tests und nicht für Integrationstests. Das mache ich gerade,

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

Wo der RouterObj gerade ist {router: expressRouter, path: '/api'}. Ich lade dann Subrouter mit ein var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));und dann ruft die Express-App eine Init-Funktion auf, die den Express-Router als Parameter verwendet. Der initRouter ruft dann router.use(loginRouterInfo.path, loginRouterInfo.router);auf, um den Subrouter zu mounten.

Der Subrouter kann getestet werden mit:

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
quelle
3
Das sieht wirklich interessant aus. Haben Sie weitere Beispiele für die fehlenden Teile, um zu zeigen, wie dies alles zusammenpasst?
CJBarth
1

Um Unit-Tests anstelle von Integrationstests zu erreichen, habe ich das Antwortobjekt des Request-Handlers verspottet.

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

Um dann Integrationstests durchzuführen, können Sie Ihren EndpointHandler verspotten und den Endpunkt mit Supertest aufrufen .

fxlemire
quelle
0

In meinem Fall wollte ich nur testen, ob der richtige Handler aufgerufen wurde. Ich wollte den Supertest verwenden, um die Einfachheit der Anforderungen an die Routing-Middleware zu verdeutlichen. Ich verwende Typescript a und dies ist die Lösung, die für mich funktioniert hat

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

Die Routen

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

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

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

Ich hatte einige Probleme, um die Verspottung zum Laufen zu bringen. Aber mit jest.doMock und der spezifischen Reihenfolge, die Sie im Beispiel sehen, funktioniert es.

Alvaro
quelle