หนึ่งหน่วยทดสอบเส้นทางด้วย Express อย่างไร


101

ผมอยู่ในขั้นตอนของการเรียนรู้ Node.js และได้รับการเล่นรอบกับเอ็กซ์เพรส ชอบกรอบมากอย่างไรก็ตามฉันมีปัญหาในการหาวิธีเขียนหน่วย / การทดสอบการรวมสำหรับเส้นทาง

ความสามารถในการหน่วยโมดูลที่เรียบง่ายการทดสอบเป็นเรื่องง่ายและได้รับการทำมันด้วย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, data"

  1. ฉันทำผิดตรงไหนเพื่อที่ฉันจะได้ทำงานนี้

  2. มีวิธีที่ดีกว่าสำหรับฉันในการทดสอบหน่วยรหัสในระดับนี้หรือไม่

อัปเดต 1. แก้ไขข้อมูลโค้ดเนื่องจากตอนแรกฉันลืม "it ()"

คำตอบ:


21

เปลี่ยนวัตถุตอบกลับของคุณ:

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

และมันจะทำงาน


4
นี่คือหน่วยทดสอบตัวจัดการคำขอไม่ใช่เส้นทาง
Jason Sebring

44

ตามที่คนอื่น ๆ แนะนำไว้ในความคิดเห็นดูเหมือนว่าวิธีทดสอบตัวควบคุม 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()
      });
  })
});

Upside: คุณสามารถทดสอบสแต็กทั้งหมดได้ในครั้งเดียว

ข้อเสีย: ให้ความรู้สึกและทำหน้าที่คล้ายกับการทดสอบการรวมระบบ


1
ฉันชอบสิ่งนี้ แต่มีวิธียืนยัน viewName (ตามคำถามเดิม) หรือไม่หรือเราจะต้องยืนยันเนื้อหาของการตอบกลับ
Alex

22
ฉันเห็นด้วยกับข้อเสียของคุณนี่ไม่ใช่การทดสอบหน่วย สิ่งนี้อาศัยการรวมหน่วยทั้งหมดของคุณเพื่อทดสอบ URL ของแอปพลิเคชันของคุณ
Luke H

11
ฉันคิดว่าเป็นเรื่องถูกกฎหมายที่จะบอกว่า "เส้นทาง" เป็นจริงintegrationและบางทีเส้นทางการทดสอบควรปล่อยให้เป็นการทดสอบการรวมระบบ ฉันหมายความว่าฟังก์ชันการทำงานของเส้นทางที่ตรงกับการเรียกกลับที่กำหนดไว้นั้นน่าจะได้รับการทดสอบโดย express.js แล้ว ตรรกะภายในใด ๆ สำหรับการรับผลลัพธ์สุดท้ายของเส้นทางควรเป็นโมดูลาร์ภายนอกและโมดูลเหล่านั้นควรได้รับการทดสอบหน่วย ปฏิสัมพันธ์ของพวกเขาเช่นเส้นทางควรได้รับการทดสอบการบูรณาการ คุณจะเห็นด้วย?
Aditya MP

1
เป็นการทดสอบแบบ end-to-end ไม่ต้องสงสัยเลย.
kgpdeveloper

25

ฉันได้ข้อสรุปว่าวิธีเดียวที่จะทำให้แอปพลิเคชั่นทดสอบหน่วยทดสอบจริง ๆ คือการแยกระหว่างตัวจัดการคำขอและตรรกะหลักของคุณ

ดังนั้นตรรกะของแอปพลิเคชันของคุณควรอยู่ในโมดูลแยกต่างหากที่สามารถrequireทดสอบ d และหน่วยและมีการพึ่งพาคลาสคำขอด่วนและการตอบกลับน้อยที่สุดเช่นนี้

จากนั้นในตัวจัดการคำขอคุณต้องเรียกวิธีการที่เหมาะสมของคลาสตรรกะหลักของคุณ

ฉันจะยกตัวอย่างเมื่อฉันปรับโครงสร้างแอปปัจจุบันของฉันเสร็จแล้ว!

ฉันเดาอะไรแบบนี้? (อย่าลังเลที่จะแยกส่วนสำคัญหรือแสดงความคิดเห็นฉันยังคงสำรวจสิ่งนี้อยู่)

แก้ไข

นี่คือตัวอย่างเล็ก ๆ แบบอินไลน์ ดูส่วนสำคัญสำหรับตัวอย่างโดยละเอียดเพิ่มเติม

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

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
คุณช่วยแก้ไขลิงก์ "ตัวช่วย" ได้ไหม
Nicholas Murray

16
ดูเหมือนว่าแนวทางที่ทันสมัยกว่าในการทดสอบหน่วย HTTP คือการใช้supertestโดย Visionmedia ดูเหมือนว่าตัวช่วย http ของ TJ ได้พัฒนาไปสู่ ​​supertest
Akseli Palén

2
supertest บน github สามารถพบได้ที่นี่
Brandon

@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);ให้ติดตั้งส่วนย่อย

สามารถทดสอบ subrouter ได้ด้วย:

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.