Як одна одиниця тестує маршрути за допомогою Express?


99

Я в процесі вивчення Node.js і бавився з Express . Дуже подобається фреймворк; однак у мене виникають проблеми з тим, як написати модульний / інтеграційний тест для маршруту.

Можливість модульного тестування простих модулів дуже проста, і це було зроблено разом з Mocha ; однак, мої модульні тести з Express не вдаються, оскільки об'єкт відповіді, який я передаю, не зберігає значень.

Тестова функція маршруту (маршрути / index.js):

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

Модуль модульного тестування:

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

    });
});

Коли я запускаю це, це не відповідає "Помилка: виявлені глобальні витоки: viewName, дані".

  1. Де я помиляюся, щоб я міг це зробити?

  2. Чи є кращий спосіб для мене перевірити свій код на цьому рівні?

Оновлення 1. Виправлений фрагмент коду, оскільки я спочатку забув "it ()".

Відповіді:


21

Змініть об’єкт відповіді:

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

І це спрацює.


2
Це модульне тестування обробника запитів, а не маршруту.
Джейсон Себрінг,

43

Як рекомендували інші в коментарях, схоже, канонічним способом тестування контролерів Express є супертест .

Приклад тесту може виглядати так:

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

Перевернуте: ви можете протестувати весь свій стек за один раз.

Недолік: він відчуває себе і діє трохи як тестування інтеграції.


1
Мені це подобається, але чи є спосіб затвердити viewName (як у вихідному питанні) - чи нам доведеться стверджувати щодо змісту відповіді?
Олексій

20
Я згоден з вашим мінусом, це не модульне тестування. Це покладається на інтеграцію всіх ваших модулів для перевірки URL-адрес вашого додатка.
Luke H

10
Я вважаю законним сказати, що "маршрут" насправді є integration, і, можливо, тестування маршрутів слід залишити інтеграційним тестам. Я маю на увазі, функціональність маршрутів, що відповідають визначеним їм зворотним викликам, імовірно, вже перевірена express.js; будь-яка внутрішня логіка для отримання кінцевого результату маршруту, в ідеалі, повинна модулюватися поза ним, і ці модулі повинні бути модульно перевірені. Їх взаємодія, тобто маршрут, має бути перевіреним на інтеграцію. Чи погодились би ви?
Aditya MP

1
Це наскрізне тестування. Без сумніву.
kgpdeveloper

23

Я прийшов до висновку, що єдиний спосіб реально одиничного тестування експрес-додатків - підтримувати велику відстань між обробниками запитів та основною логікою.

Таким чином, логіка вашого додатку повинна бути в окремих модулях, які можна requireперевірити і модульно протестувати, і мати мінімальну залежність від класів Express Request та Response як таких.

Тоді в обробниках запитів вам потрібно викликати відповідні методи ваших основних класів логіки.

Я наведу приклад, коли закінчу реструктуризую свою поточну програму!

Я здогадуюсь щось подібне ? (Не соромтеся розгалужувати суть або коментувати, я все ще вивчаю це).

Редагувати

Ось крихітний приклад, вбудований. Дивіться суть для більш детального прикладу.

/// 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
На мій погляд, це найкращий шаблон для використання. Багато веб-фреймворків на різних мовах використовують шаблон контролера, щоб відокремити бізнес-логіку від реальної функції формування відповіді http. Таким чином, ви можете просто перевірити логіку, а не весь процес відповіді http, що розробники фреймворку повинні тестувати самостійно. Інші речі, які можна перевірити за цією схемою, - це прості проміжні засоби, деякі функції перевірки та інші бізнес-послуги. Проте тестування підключення до БД - це зовсім інший тип тестування
OzzyTheGiant,

1
Дійсно, багато відповідей тут справді пов'язані з інтеграцією / функціональним тестуванням.
Luke H

19

Найпростіший спосіб протестувати HTTP за допомогою express - це викрасти помічника http TJ

Я особисто користуюся його помічником

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

Якщо ви хочете спеціально протестувати об’єкт маршрутів, передайте правильні макети

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
Ви могли б виправити посилання "помічник"?
Ніколас Мюррей,

16
Здається, що більш сучасний підхід до тестування модулів HTTP полягає у використанні супертесту від Visionmedia. Також здається, що помічник http від TJ перетворився на супертест.
Акселі Пален

2
супертест на github можна знайти тут
Брендон

@Raynos, чи можете ви пояснити, як ви отримуєте запит та додаток у своєму прикладі?
jmcollin92

9
На жаль, це інтеграційне тестування, а не модульне тестування.
Luke H

8

якщо модульне тестування за допомогою express 4 зверніть увагу на цей приклад від 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

Мені це теж було цікаво, але спеціально для модульних тестів, а не для інтеграційних тестів. Це те, що я роблю зараз,

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

Де просто routerObj {router: expressRouter, path: '/api'}. Потім я завантажую в під маршрутизатори, var loginRouterInfo = require('./login')(express.Router({mergeParams: true}));а потім експрес-програма викликає функцію init, приймаючи експрес-маршрутизатор як параметр. Потім initRouter закликає router.use(loginRouterInfo.path, loginRouterInfo.router);встановити під маршрутизатор.

Підрутер можна протестувати за допомогою:

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
Це виглядає по-справжньому цікаво. Чи є у вас більше прикладів відсутніх частин, щоб показати, як це все поєднується?
cjbarth

1

Щоб досягти модульного тестування замість інтеграційного тестування, я знущався над об’єктом відповіді обробника запиту.

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

Потім, для досягнення інтеграційного тестування, ви можете знущатись над endpointHandler і викликати кінцеву точку за допомогою супертесту .


0

У моєму випадку єдине, що я хотів перевірити, - це те, чи був викликаний правий обробник. Я хотів використовувати супертест, щоб використати простоту надсилання запитів до проміжного програмного забезпечення для маршрутизації. Я використовую Typescript a, і це рішення, яке працювало для мене

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

Маршрути

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

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

Тести

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

У мене були деякі проблеми, щоб знущання запрацювало. але використання jest.doMock і конкретний порядок, який ви бачите в прикладі, змушує це працювати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.