Controller (phần quản lý logic) là thành phần rất quan trọng trong ứng dụng Express. Một trang web chỉ trả về các lỗi 404 hay 500 thì chẳng thể nào giữ nổi khách hàng. Thế nên controller xứng đáng được kiểm thử. So với các thành phần khác trong ứng dụng, controller không phải là thành phần độc lập. Nó phụ thuộc vào rất nhiều thứ: models, các dịch vụ bên ngoài. Điều này làm việc kiểm thử có một chút khó khăn nhưng bạn không cần phải lo lắng, ngay sau đây mình sẽ hướng dẫn bạn cách kiểm thử nó cẩn thận trước khi triển khai ứng dụng.

Cái gì ảnh hưởng đến controller?

Controller chịu ảnh hưởng từ 3 thành phần chính:

  • request từ người dùng: Nó bao gồm dữ liệu người dùng gửi lên server, dữ liệu trên session, hành vi đặc biệt của người dùng với ứng dụng...
  • Models: Tất nhiên là controller sẽ phải lấy dữ liệu từ databse, cũng như xử lý khá nhiều các logic hệ thống trên đó. Kết quả bạn nhận được không chỉ phụ thuộc vào database mà còn cả các logic của ứng dụng.
  • Ứng dụng từ bên thứ 3: Với kiến trúc microservices controller của bạn có thể gọi đến khá nhiều các dịch vụ khác nhau để lấy dữ liệu. Bên cạnh đó nếu ứng dụng của bạn gọi đến 1 dịch vụ thứ 3 như Twitter hay Facebook, kết quả bạn nhận được từ controller khá khó kiểm soát.

Làm cách nào để kiểm thử controller?

Như các thành phần khác trong ứng dụng, thông thường ta phải kiểm thử từng phần một cách độc lập. Tuy nhiên như ở trên đã nói, controller có quá nhiều yếu tố phụ thuộc.

Giải pháp của chúng ta cho vấn đề này là phải mock lại các thành phần phụ thuộc này bên cạnh đó bạn cũng phải tìm cách để kiểm soạt chúng.

Mock: Bạn nào biết cuốn sách "To Kill a Mockingbird" (Giết con chim nhại). Đây là 1 kỹ thuật nhại lại, mô phỏng lại hàm hoặc đối tượng. Có 3 cái kỹ thuật mô phỏng là stub, spy và mock

Cài đặt công cụ

Hi vọng là bạn không nghĩ đến việc thử thổ dân bằng tay qua việc dùng browser. Có khá nhiều công cụ kiểm thử ứng dụng nodejs. Trong bài này chúng ta sẽ sử dụng mocha module chuyên về kiểm thử, nó do cha đẻ của Express phát triển (TJ Holowaychuk). Bên cạnh đó mình cũng sẽ sử dụng should một thư viện assert với rất nhiều các hàm hỗ trợ thú vị.

Assert - Các công cụ để đánh giá dữ liệu, các công cụ này có cú pháp gần với ngữ pháp tiếng anh giúp cho việc viết Test đơn giản hơn (với mình thì ...)

Cài đặt 2 module này trong ứng dụng của bạn ở dev-dependencies.

Dev dependencies là các thư viện dành cho việc phát triển. Khi triển khai ứng dụng ta có thể bỏ qua việc cài đặt các gói này giúp ứng dụng nhẹ nhàng hơn.

$ npm install mocha should --save-dev

Hãy add thêm 1 đoạn script vào trong file package.json để đơn giản việc kiểm thử.

"scripts": { 
    "test" : "./node_modules/.bin/mocha tests/**" 
}

Với việc này bạn có chỉ cần gọi npm test trong terminal là ứng dụng sẽ chạy kiểm thử.
Mình khuyến khích các bạn cài mocha global

  $ sudo npm install mocha -g

ở trong script ở trên bạn có thể thay thế cú pháp thành

"scripts": { 
    "test" : "mocha tests/**" 
}

Ở trên chúng ta cũng cấu hình thư mục load các file kiểm thử là tests

Tạo một ứng dụng Express đơn giản

Học "Xây dựng web site tốc độ cao bằng Node.js" được học miễn phí khóa "HTML5, CSS3, JavaScript"

Đầu tiên hãy tạo file app.js

var express = require("express");
var app = express();

var controller = require("./controllers/welcome");

app.use('/',controller);

app.listen(3333, function () {
    console.log("Server started")
});

Bên cạnh đó mình tạo một file controller đơn giản là welcome theo thư mục trên nhé.

var express = require('express'),
    router = express.Router();

router.get('/hello', function(req, res) {
    res.send('world')
});

router.get('/upper/:word', function(req, res) {
    res.send(req.params.word.toUpperCase())
});

module.exports = router;

Ở file route trên các bạn có thấy 2 controller đi kèm. Một controller trả về "word" còn một cái khác in ra dữ liệu param trên url. Các bạn có thể thấy hai thành phần chúng ta cần mock ở đây là request và response.

Mocking với request, response với node-mocks-http

Module mình sử dụng ở đây để mock là node-mocks-http. Nó sẽ giúp bạn tạo các đối tượng request và response.

    $ npm install node-mocks-http --save-dev

Tạo ra file test đầu tiên:

var controller = require('../controllers/welcome')
    , http_mocks = require('node-mocks-http')
    , should = require('should');

function buildResponse() {
    return http_mocks.createResponse({eventEmitter: require('events').EventEmitter})
}

describe('Welcome Controller Tests', function() {

    it('hello', function(done) {
        var response = buildResponse();
        var request  = http_mocks.createRequest({
            method: 'GET',
            url: '/hello'
        });

        response.on('end', function() {
            response._getData().should.equal('world');
            done()
        });

        controller.handle(request, response)
    });

    it('hello fail', function(done) {
        var response = buildResponse();
        var request  = http_mocks.createRequest({
            method: 'POST',
            url: '/hello'
        });

        response.on('end', function() {
            // POST method should not exist.
            // This part of the code should never execute.
            done(new Error("Received a response"))
        });

        controller.handle(request, response, function() {
            done()
        })
    });

    it('upper', function(done) {
        var response = buildResponse();
        var request  = http_mocks.createRequest({
            method: 'GET',
            url: '/upper/monkeys'
        });

        response.on('end', function() {
            response._getData().should.equal('MONKEYS');
            done()
        });

        controller.handle(request, response)
    })
});

Tìm hiểu sâu một chút về đoạn code ở trên nhé:

describe và it là hai cú pháp cơ bản để kiểm thử ứng dụng với mocha (Bạn có thể gặp cú pháp này ở khá nhiều các test framework khác, mà bạn nào làm tester rồi thì chắc không cần đọc bài này rồi :D). Nó giúp bạn mô tả rõ ràng các hành vi của controller. Ở đây describe mô tả thành phần bạn muốn kiểm thử, nó bao gồm it là các trường hợp có thể xảy ra. Với mỗi it bạn truyền vào param là done, đây là hàm callback bạn phải gọi khi kết thúc việc kiểm thử 1 trường hợp

Ở đây mình tạo một hàm hỗ trợ là buildResponse giúp mình tạo ra một đối tượng response với thư viện node-mocks-http, mình gắn cho nó thêm một đối tương EventEmitter. Tại sai thế nhỉ? Điều này giúp cho bạn có thể biết được khi nào controller của bạn hoàn thành xong nhiệm vụ của nó và trả về đối tượng response. Đó là lúc nó emit sự kiện end. Chức năng này khá hữu dụng khi controller gọi bất đồng bộ đến cơ sở dữ liệu đến database hoặc các dịch vụ web, nó giúp bạn tránh phải viết cú pháp callback quá lằng nhằng (dính callback hell rồi thì chắc cũng chẳng còn muốn kiểm thử nữa nhỉ).

Trong mỗi trường hợp it, chúng ta cũng sẽ tạo ra đối tượng request trong đó bao gồm url và method. Khi controller xử lý xong, sự kiện end được gọi, chúng ta check kết quả nhận được đồng thời gọi đến hàm done để kết thúc việc kiểm tra trường hợp đó. Để controller tiến hành xử lý chúng ta cần gọi đến hàm handle như cú pháp bên trên. Hàm này sẽ thực hiện controller dựa theo cái request mà ta tạo ra.

Trong trường hợp mình thử gửi 1 hàm POST vào route /hello mình kì vọng là nó sẽ không thể chạy được. Với một ứng dụng express bình thường các hàm xử lý route Post thường là các middleware có hàm next đi kèm, ở đây ta mô phỏng lại việc đó bằng thêm hàm callback ở hàm handle và gọi done ở đây (hiểu đơn giản nếu thành công thì ta gọi next được). Trong bài kiểm thử sau ta sẽ thấy rõ điều này.

Hãy lưu nội dung  trên vào file "request.js" trong thư mục tests nhé. Chạy thử nào.

    $ npm test

Học lập trình web bằng Nodejs

 

 

Kết quả bạn nhận được tương tự trong hình.

Tạo controller models cho ứng dụng của bạn

Hãy làm ứng dụng của bạn phức tạp hơn với thành phần thứ 2: models.

Mình tạo 1 controller mới với trong thư mục controllersnews.js

var express = require('express'),
     router = express.Router(),
     model = require('../models/news');

router.get('/all', function(req, res) {
    model.all(function(err, items) {
        if (err) return res.json({error: "There is a problem"});
        res.json({error: null, news: items})
    })
});

router.post('/create', function(req, res) {
    model.create(req.body.title, req.body.text, function(err, doc) {
        if (err) return res.json({error: "There is a problem"});
        res.json({error: null, news: doc})
    })
});

module.exports = router;

Ở đây mình sẽ không tạo file database mà sẽ sử dụng mock models

Mock model với mockery

Cài đặt cái module phụ trợ này đã:

    $ npm install mockery --save-dev

Sực mạnh của mockery nằm ở việc bạn có thể thay đổi object trả về từ hàm require. Sao ta phải làm vậy nhỉ? Như các bạn thấy trong controller news.js mình có gọi đến models/news nhưng mình hoàn toàn chưa viết cái models này thế nên mình sẽ dùng mockery để vượt qua cái require khó chịu này nhé.

Để có thể bỏ qua cái require models mình cần gọi 2 hàm mockery.enable() và mockery.disable(). Các bạn sẽ rõ hơn khi xem đoạn code kiểm thử của models.js

var http_mocks = require('node-mocks-http'),
    should = require('should'),
    mockery = require('mockery');

function buildResponse() {
    return http_mocks.createResponse({eventEmitter: require('events').EventEmitter})
}

describe('News Controller Tests', function () {

    before(function () {
        mockery.enable({
            warnOnUnregistered: false
        });

        mockery.registerMock('../models/news', {
            all: (cb) => cb(null, ["First news", "Second news"]),
            create: (title, text, cb) => cb(null, {title: title, text: text, id: Math.random()})
        });

        this.controller = require('../controllers/news')
    });

    after(function () {
        mockery.disable()
    });

    it('all', function (done) {
        var response = buildResponse();
        var request = http_mocks.createRequest({
            method: 'GET',
            url: '/all'
        });

        response.on('end', function () {
            response._isJSON().should.be.true;

            var data = JSON.parse(response._getData());
            should.not.exist(data.error);
            data.news.length.should.eql(2);
            data.news[0].should.eql("First news");
            data.news[1].should.eql("Second news");

            done()
        });

        this.controller.handle(request, response)
    });

    it('create', function (done) {
        var response = buildResponse();
        var request = http_mocks.createRequest({
            method: 'POST',
            url: '/create'
        });

        request.body = {
            title: "Something is happening",
            text: "Something is happening in the world!"
        };

        response.on('end', function () {
            response._isJSON().should.be.true;

            var data = JSON.parse(response._getData());
            should.not.exist(data.error);
            data.news.title.should.eql(request.body.title);
            data.news.text.should.eql(request.body.text);
            data.news.id.should.exist;

            done()
        });

        this.controller.handle(request, response)
    });
});

Ở đây các bạn sẽ thấy 2 hàm mới việc kiểm thử là before và after. Ý nghĩa của chúng đơn giản như tên gọi của nó vậy. Một cái là chạy trước khi bắt đầu bài test, cái còn lại là chạy sau khi bắt đầu bài test.

Bài viết này sử dụng cú pháp arrow function của ES6. Bạn nào đang chạy node bản 0.12 thì nâng cấp lên đi nhé 4.2.1 rồi đấy

    (cb) => cb(null, ["First news", "Second news"])

đơn giản là cách viết ngắn gọn của

function(cb) {
    return cb(null, ["First news", "Second news"])
}

Ở đoạn code trên mình vẫn dùng node-mocks-http để tạo request

Để có thể mock models mình gọi mockery.enable() trong before.

Đối tượng mình trả về là một Object có 2 function createall tương tự như 1 models vậy. Chú ý gọi mockery.disable() nhé vì cơ chế của require là cache mà nó sẽ ảnh hưởng đến các bài test của các bạn có dùng thằng models news.

Trong it đầu tiên,chúng ta thực hiện việc lấy dữ liệu từ database. Với đối tượng mock ta hoàn toàn kiểm soát được dữ liệu trả về là kiểm tra controller xem nó có hoạt động đúng không.

Ở thằng it thứ hai, chúng ta mô phỏng việc đẩy dữ liệu vào database. Ở đây ta kiểm soát dữ liệu đầu vào bằng node-mocks-http request, biết dữ liệu đầu ra từ mockery việc còn lại là check logic của controller đúng hay sai thôi :D.

 

Học lập trình web trực tuyến

 

 

Mocking dịch vụ với nock

Thành phần cuối cùng ta cần mock là việc gọi đến các dịch vụ bên ngoài ứng dụng. Sử dụng kỹ thuật mock với vấn đề này vì ta kiểm soát được đầu ra của dịch vụ, mặt khác kết nối thực tế tới dịch vụ có thể chậm làm giảm hiệu năng làm việc.

Cài đặt module hỗ trợ trước đã:

    $ npm install nock --save-dev   

Tiếp đó phải tạo 1 controller để test thử nữa

controllers/services.js

var express = require('express'),
    router = express.Router(),
    request = require('request');
router.get('/service', function(req, res) {
    request('http://www.google.com', function (error, response, body) {
        if (!error && response.statusCode == 200) {
            res.send(body)
        }
    })
});

module.exports = router;

Bước cuối cùng là tạo file kiểm thử.
test/services.js

var controller = require('../controllers/services'),
    http_mocks = require('node-mocks-http'),
     should = require('should'),
    nock = require('nock');


function buildResponse() {
    return http_mocks.createResponse({eventEmitter: require('events').EventEmitter})
}

describe('Services Controller Tests', function() {

    it('Service', function(done) {
        nock('http://www.google.com').get('/').reply(200, 'something funny');

        var response = buildResponse();
        var request  = http_mocks.createRequest({
            method: 'GET',
            url: '/service'
        });

        response.on('end', function() {
            response._getData().should.eql('something funny')
            done()
        });

        controller.handle(request, response)
    })
});

Phù cuối cùng cũng xong. Đoạn code khá dễ hiểu phải không nào. Nock ở đây giúp bạn giả lập việc gọi lên thằng google để lấy thông tin, bạn có thể thiết lập các dữ liệu nó trả về nên việc test controller giờ không còn khó khăn nữa;

Kết quả của 3 bài test :D.

Học lập trình web trực tuyến bằng Nodejs
 

Bài viết được dịch và chú thích từ : https://www.terlici.com/2015/09/21/node-express-controller-testing.html