하나의 단위 테스트는 Express로 어떻게 라우팅됩니까?


99

저는 Node.js를 배우는 과정에 있으며 Express를 가지고 놀았습니다 . 프레임 워크와 정말 비슷하지만 경로에 대한 단위 / 통합 테스트를 작성하는 방법을 알아내는 데 어려움이 있습니다.

간단한 모듈을 단위 테스트 할 수 있다는 것은 쉽고 Mocha 와 함께 해왔습니다 . 그러나 전달하는 응답 객체가 값을 유지하지 않기 때문에 Express를 사용한 단위 테스트가 실패합니다.

테스트중인 경로 함수 (routes / 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;
    }
};

그리고 그것은 작동 할 것입니다.


4
이것은 라우트가 아니라 요청 핸들러를 단위 테스트하는 것입니다.
Jason Sebring

43

다른 사람들이 의견에서 권장했듯이 Express 컨트롤러를 테스트하는 표준 방법은 supertest 를 사용하는 것 같습니다 .

예제 테스트는 다음과 같습니다.

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을 주장하는 방법이 있습니까 (원래 질문에서와 같이)-아니면 응답의 내용에 대해 주장해야합니까?
Alex

21
나는 당신의 단점에 동의합니다. 이것은 단위 테스트가 아닙니다. 이는 애플리케이션의 URL을 테스트하기 위해 모든 단위의 통합에 의존합니다.
Luke H

10
나는 "경로"가 실제로라고 말하는 것이 합법적이라고 생각 integration하며 아마도 테스트 경로는 통합 테스트에 맡겨야합니다. 내 말은, 정의 된 콜백과 일치하는 경로의 기능은 아마도 이미 express.js에 의해 테스트되었을 것입니다. 경로의 최종 결과를 얻기위한 내부 로직은 이상적으로 외부에서 모듈화되어야하며 이러한 모듈은 단위 테스트를 거쳐야합니다. 이들의 상호 작용, 즉 경로는 통합 테스트를 거쳐야합니다. 동의 하시겠습니까?
Aditya MP

1
종단 간 테스트입니다. 의심 할 여지가 없습니다.
kgpdeveloper

25

실제로 단위 테스트 익스프레스 애플리케이션을 수행하는 유일한 방법은 요청 핸들러와 핵심 로직 사이에 많은 분리를 유지하는 것뿐이라는 결론에 도달했습니다.

따라서 응용 프로그램 논리는 required 및 단위 테스트 가 가능한 별도의 모듈에 있어야하며 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 응답 프로세스가 아닌 로직 만 테스트 할 수 있습니다. 이는 프레임 워크 개발자가 스스로 테스트해야하는 부분입니다. 이 패턴에서 테스트 할 수있는 다른 것들은 간단한 미들웨어, 일부 유효성 검사 기능 및 기타 비즈니스 서비스입니다. DB 연결 테스트는하지만 시험의 완전히 다른 유형
OzzyTheGiant

1
실제로 여기에있는 많은 답변은 통합 / 기능 테스트와 관련이 있습니다.
Luke H

이것은 정답입니다. Express가 아닌 로직 테스트에 집중해야합니다.
esmiralha

19

Express로 HTTP를 테스트하는 가장 쉬운 방법은 TJ의 http 도우미 를 훔치는 것입니다.

나는 개인적으로 그의 도우미를 사용합니다

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
'도우미'링크를 고칠 수 있습니까?
Nicholas Murray

16
HTTP 단위 테스트에 대한 최신 접근 방식은 Visionmedia의 supertest 를 사용 하는 것 같습니다. 또한 TJ의 http 도우미가 슈퍼 테스트로 진화 한 것 같습니다.
Akseli Palén 2013 년

2
GitHub의에 supertest를 찾을 수 있습니다 여기에
브랜든

@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}));한 다음 익스프레스 앱은 익스프레스 라우터를 매개 변수로 사용하는 초기화 함수를 호출합니다. 그런 다음 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를 모의하고 supertest를 사용 하여 엔드 포인트를 호출 할 수 있습니다 .


0

제 경우에는 올바른 핸들러가 호출되었는지 테스트하고 싶었습니다. 라우팅 미들웨어에 요청하는 단순성을 활용하기 위해 supertest를 사용하고 싶었습니다. 나는 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과 예제에서 보는 특정 순서를 사용하면 작동합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.