Recent Posts

Unit Test Với AngulaJS

-

Thiết lập môi trường Unit Testing

Karma - môi trường để chạy Unit Testing

(Vai trò của Karma đối với việc Kiểm thử trong AngularJS cũng giống như nền móng và mặt bằng xung quanh của một ngôi nhà)

Cài đặt Node.js

Các bạn có thể tham khảo hướng dẫn cài đặt Node.js tại trang hướng dẫn này.

Cài đặt Karma

Dưới đây tôi chỉ ghi chép lại các bước cài đặt mà tôi đã thực hiện trên Ubuntu. Đối với các hệ điều hành khác, các bạn vui lòng tham khảo trên Internet. Trừ khi sử dụng nvm (Node Version Manager), các bạn chỉ nên cài đặt karma ở chế độ local (không dùng tham số -g): Karma và tất cả plugin liên quan đều được cài đặt trong thư mục node_modules bên trong thư mục gốc của dự án.
Về cơ bản, chúng ta cần các gói thư viện karmakarma-jasmine và karma-chrome-launcher (hoặc karma-firefox-launcher nếu bạn muốn chạy với Firefox) , các thư viện phụ thuộc sẽ được npm tự động tìm và cài đặt:
$ npm install karma --save-dev
$ npm install karma-jasmine karma-chrome-launcher --save-dev
Tham số –save-dev lưu thông tin của karma vào khai báo devDependencies trong tệp package.json.
Sau khi cài đặt xong, chạy thử Karma như sau:
$ ./node_modules/karma/bin/karma start

Cài đặt Karma Commandline Interface

Để tránh mỗi lần muốn chạy Karma, người dùng phải gõ lệnh ./node_modules/karma/bin/karma start dài và nhàm chán, chúng ta cài đặt Karma Commandline Interface (lưu ý các tham số -g):
$ npm install -g karma-cli
Sau đó, mỗi lần muốn chạy Karma, chúng ta chỉ cần sử dụng lệnh ngắn gọn, chẳng hạn karma startkarma-clisẽ tự động tìm ứng dụng karma tại thư mục đang làm việc (tức là ./node_modules/karma/bin/karma) để thực thi.

Tệp cấu hình của Karma

Karma cho phép người dùng tùy chỉnh môi trường cho các dạng kiểm thử khác nhau, bằng cách cung cấp lệnh để người dùng khai báo các tham số môi trường. Các khai báo tham số môi trường kiểm thử sẽ được lưu trong một tệp cấu hình. Thông thường, người lập trình sẽ khai báo tham số và lưu vào tệp cấu hình môi trường cho mỗi loại kiểm thử trong AngularJS, chẳng hạn karma-unit.conf.js cho Unit Testing, karma-e2e.conf.js cho End-to-End Testing.
Để thuận tiện cho việc khai báo, Karma cung cấp lệnh karma init <tên tệp cấu hình> để người dùng chọn giá trị cho từng mục cấu hình. Chẳng hạn, để tạo tệp cấu hình cho Unit Testing, chúng ta chuyển thư mục làm việc vào thư mục gốc của dự án, dùng lệnh khởi tạo như sau:
$ karma init karma-unit.conf.js
(Lưu ý chỉ tạo tệp cấu hình sau khi đã cài đặt karma và các gói thư viện liên quan vào thư mục gốc của dự án như hướng dẫn ở bên trên).

Jasmine - khung ứng dụng cho Unit Testing

(Vai trò của Jasmine đối với việc Kiểm thử trong AngularJS cũng giống như khung bê-tông, dầm trụ xi-măng của một ngôi nhà)
Jasmine là khung ứng dụng hỗ trợ kiểm thử theo mô hình BDD (Behavior-Driven Development framework). Jasmine được chọn mặc định làm khung ứng dụng kiểm thử trong Karma. Để sử dụng Jasmine với Karma, chúng ta cần cài đặt gói thư viện karma-jasmine (như đã thực hiện trong phần cài đặt Karma ở bên trên).
Jasmine cung cấp nhiều hàm tiện ích hỗ trợ các khía cạnh khác nhau trong kiểm thử. Phần dưới đây sẽ trình bày các nhóm hàm hỗ trợ kiểm thử thường dùng của Jasmine.

Khai báo Spec Suite

Spec Suite là một bộ mô tả kiểm thử, bao gồm nhiều hàm kiểm thử, mỗi hàm kiểm thử tập trung vào một khía cạnh cần kiểm tra của một đơn vị chương trình. Các hàm kiểm thử trong Spec Suite thường có chung đơn vị chương trình nên có môi trường thử nghiệm giống nhau, do đó Spec Suite chứa các hàm khởi tạo và các hàm thu dọn môi trường chung cho các hàm kiểm thử đó. Spec Suite được định nghĩa bởi hàm describe():
describe('Spec Suite: myController', function() {

// các hàm khởi tạo, thu dọn môi trường

// các hàm kiểm thử từng khía cạnh của đơn vị chương trình
});
Chúng ta có thể phân nhỏ các Spec Suite bằng cách định nghĩa các Spec Suite lồng nhau:
describe('Spec Suite: myController', function() {

describe('Sub Spec Suite: create function', function() {
// ...
});

describe('Sub Spec Suite: update function', function() {
// ...
});
});

Khai báo hàm kiểm thử (Spec)

Trong Jasmine, mỗi hàm kiểm thử được gọi là một Spec. Jasmine cung cấp hàm it() để định nghĩa Spec.
describe('Spec Suite: a very simple Spec', function() {
it('contains a passing spec', function() {
expect(true).toBe(true);
});
});

Các hàm khởi tạo và thu dọn môi trường

Để thiết lập môi trường kiểm thử cho các Spec, Jasmine cung cấp hàm beforeEach() cho phép khai báo đoạn mã lệnh sẽ được chạy ngay trước khi thực hiện mỗi Spec. Cụ thể, beforeEach() thường được dùng để giả lập các Service mà đối tượng được kiểm thử cần dùng đến, xác lập trạng thái ban đầu của đối tượng,v.v.. Hàm beforeEach() được định nghĩa như sau:
describe('Spec Suite: myController', function() {

beforeEach(function() {
// setup something before running each Spec ...
});
});
Tương tự như vậy, Jasmine cung cấp hàm afterEach() để khai báo hàm sẽ được chạy ngay sau khi thực hiện từng Spec:
describe('Spec Suite: myController', function() {

afterEach(function() {
// reset environment after running each Spec ...
});
});

Các hàm kỳ vọng (Expectations)

Các hàm kỳ vọng cho phép chúng ta khẳng định một biểu thức cần kiểm thử phải có giá trị thỏa mãn một yêu cầu nào đó.
  • expect(x).toEqual(val): Khẳng định giá trị của đối tượng x bằng với val (nhưng không nhất thiết đồng nhất nhau).
  • expect(x).toBe(obj): Khẳng định rằng đối tượng x đồng nhất với obj (2 đối tượng này là một).
  • expect(x).toMatch(regexp): Khẳng định chuỗi x khớp với biểu thức chính quy regexp.
  • expect(x).toBeNull(): Khẳng định rằng biến x chứa giá trị là null.
  • expect(x).toBeTruthy(): Khẳng định rằng giá trị x là true hoặc ước lượng bằng true.
  • expect(x).toBeFalsy(): Khẳng định rằng giá trị x là false hoặc ước lượng bằng false.
  • expect(x).toContain(y): Khẳng định rằng x là một chuỗi ký tự và x chứa giá trị y (chuỗi y là một phần của chuỗi x).
  • expect(x).toBeGreaterThan(y): Khẳng định rằng x lớn hơn y.
  • expect(x).toBeDefined(): Khẳng định rằng biến x đã được định nghĩa.
  • expect(x).toBeUndefined(): Khẳng định rằng biến x chưa được định nghĩa.

Ví dụ đơn giản về Unit Testing

Chúng ta tạo một ví dụ đơn giản sử dụng Karma để kiểm thử cho một chức năng (lưu ý là chưa liên quan đến AngularJS). 
Trước tiên, tạo một thư mục có tên example-karma-simple-unit-testing, sau đó chuyển vào làm việc trong thư mục này:
$ mkdir example-karma-simple-unit-testing
$ cd example-karma-simple-unit-testing
$ npm install karma --save-dev
$ npm install karma-jasmine karma-chrome-launcher --save-dev
$ karma init karma-unit.conf.js
$ mkdir test
$ mkdir test/unit
$ touch test/unit/simpleSpec.js
Mở phần mềm soạn thảo (geditgeany, v.v.) để thêm mã lệnh kiểm thử sau vào tệp simpleSpec.js:
//
// file: example-karma-simple-unit-testing/test/unit/simpleSpec.js
//
describe("A very simple Unit testing", function () {

var counter;

// Preparation (set up a scenario)
beforeEach(function () {
counter = 0;
});

it("Increments value", function () {
// attempt the operation
counter++;
// verify the result
expect(counter).toEqual(1);
})

it("Decrements value", function () {
// attempt the operation
counter--;
// verify the result
expect(counter).toEqual(-1);
})
});
Quay lại cửa sổ dòng lệnh và chạy Karma để kiểm thử:
$ karma start karma-unit.conf.js

Áp dụng Unit Testing cho ứng dụng AngularJS

Ứng dụng xây dựng bằng AngularJS có các thành phần sau chứa mã lệnh cần được kiểm thử:
  • Controller (bao gồm cả Scope)
  • Service
  • Filter
  • Directive
AngularJS xem mỗi Controller, Service, Filter và Directive là một đơn vị chương trình, do đó cần được áp dụng kiểm thử đơn vị trong quá trình phát triển. Phần nội dung này của bài viết sẽ tập trung trình bày các khái niệm cũng như những kỹ thuật cơ bản để áp dụng kiểm thử đơn vị và các thành phần trên của AngularJS.

Kỹ thuật giả lập trong kiểm thử AngularJS

Trước khi tìm hiểu cách thức áp dụng kiểm thử đơn vị vào các thành phần của AngularJS, chúng ta cần hiểu rõ một kỹ thuật quan trọng trong lập trình kiểm thử: kỹ thuật giả lập (mocking). Giả lập trong kiểm thử đơn vị ứng dụng AngularJS là sự kết hợp giữa các hàm Spy của Jasmine với module ngMocks của AngularJS.

Khai báo thư viện angular-mocks

Để tạo các đối tượng giả dùng cho kiểm thử đơn vị trong AngularJS, chúng ta cần khai báo thư viện angular-mocks bằng cách bổ sung khai báo tệp angular-mocks.js trong cấu hình của Karma. Cần lưu ý thứ tự khai báo các tệp thư viện trong thuộc tính files của tệp cấu hình Karma:
//
// file: karma-unit.conf.js
//
module.exports = function(config) {
config.set({

// ...

// list of files / patterns to load in the browser
files: [
'main/lib/angular/angular.js',
'main/lib/angular/angular-mocks.js',
'main/js/*.js',
'test/**/*Spec.js'
],

// .....

});
};
Một khi đã khai báo thư viện angular-mocks, chúng ta có thể viết lệnh tạo ngữ cảnh giả lập đối tượng cho module ứng dụng Angular cần kiểm thử.

Giả lập module ứng dụng được kiểm thử

Chẳng hạn chúng ta cần kiểm thử đơn vị đối với Service có tên myService trong module ứng dụng myApp, khi đó chúng ta cần sử dụng hàm angular.mock.module() tạo ngữ cảnh giả lập đối tượng cho myApp ứng với từng lệnh kiểm thử it() như sau:
describe('myApp Unit Testing', function() {

// Mock our 'myApp' angular module
beforeEach(angular.mock.module('myApp'));

it('Unit testing 1', function() {
// ...
});

it('Unit testing 2', function() {
// ...
});
});

Thay thế các dịch vụ phụ thuộc

Quá trình kiểm thử đơn vị đòi hỏi phải cô lập thành phần đang được kiểm thử khỏi các thành phần phụ thuộc khác. Hàm angular.mock.module() cho phép mã lệnh kiểm thử lấy service $provide ra để định nghĩa lại (thay thế) các service phụ thuộc khác trước khi chạy hàm kiểm thử:
describe('myApp Unit Testing', function() {

// Mock our 'myApp' angular module
beforeEach(angular.mock.module('myApp', function($provide) {
// sử dụng các hàm factory(), service(), value() của $provide
}));

it('Unit testing 1', function() {
// ...
});

it('Unit testing 2', function() {
// ...
});
});
Có thể tìm hiểu thêm về nội dung này trong phần bài viết “Áp dụng Unit Testing cho các Service” bên dưới.

Chỉ định dịch vụ cần sử dụng

Để kiểm thử các thành phần (Controller, Service, Filter, Directive), chúng ta cần tạo ra đối tượng cụ thể của thành phần đó, thay thế các dịch vụ phụ thuộc bằng các đối tượng giả lập, thực hiện các lời gọi hàm theo trình tự của kịch bản kiểm thử.
Để có thể tiếp cận các dịch vụ của AngularJS để sử dụng phục vụ quá trình kiểm thử (chẳng hạn tạo đối tượng thành phần cần được kiểm thử như mô tả ở trên), thư viện angular-mocks cung cấp hàm angular.mock.inject()như là một điểm giao tiếp để người viết mã lệnh kiểm thử có thể chỉ định các dịch vụ của AngularJs cần dùng. Ví dụ, để có thể tạo đối tượng để kiểm thử myController, người lập trình có thể viết mã lệnh kiểm thử như sau:
describe("myController Unit testing #1", function () {
var mockScope;
var controller;

beforeEach(angular.mock.module("myApp"));

beforeEach(angular.mock.inject(function ($rootScope, $controller) {
mockScope = $rootScope.$new();
controller = $controller("myController", {
$scope: mockScope,
});
}));

it("unit testing function", function () {
// testing code using controller and mockScope variables
})
});
Lưu ý về quy ước sử dụng dấu gạch dưới (underscore - _) trong tên gọi của dịch vụ cung cấp bởi angular.mock.inject(): Đôi khi chúng ta muốn đặt tên biến tham chiếu trùng với tên của dịch vụ để thuận tiện cho việc lập trình. Chẳng hạn, cách đặt tên biến someService sau có thể gây khó hiểu cho mã lệnh bên dưới:
describe('myService Unit Testing', function (){
var someService;

beforeEach(angular.mock.inject(function(myService) {
someService = myService; // <-- we can't use the same name!
}));

// There's a lot of code here ...

it('using someService', function () {
// <-- this is more confusing: what is someService, huh?
});
});
Thư viện angular-mocks đưa ra quy ước dịch vụ truyền vào hàm angular.mock.inject() có thể được bao trong cặp dấu gạch dưới, từ đó cho phép tên biến có thể trùng với tên dịch vụ. Khi đi tìm dịch vụ để truyền vào, angular.mock.inject() sẽ tự động loại bỏ cặp dấu gạch dưới để xác định đúng tên của dịch vụ cần tìm.
describe('myService Unit Testing', function (){
var myService; // <-- we can use the same name

beforeEach(angular.mock.inject(function(_myService_) {
myService = _myService_;
}));

// There's a lot of code here ...

it('using myService', function () {
// It's ok. myService is myService
});
});

Áp dụng Unit Testing cho các Controller

Tải ví dụ minh họa

Tải ví dụ minh họa “AngularJS Controller Unit Testing” theo địa chỉ dành cho git clone hoặc bản nén zip. Sau khi tải xong ví dụ, chuyển vào thư mục example-karma-unit-testing-controller, thực hiện lệnh sau để cài đặt karma:
$ npm install karma karma-jasmine karma-chrome-launcher --save-dev
Sau khi npm cài đặt xong các gói thư viện, chạy thử ví dụ bằng lệnh:
$ karma start karma-unit.conf.js

Mô tả Controller cần kiểm thử

Giả sử chúng ta cần tạo một Controller có tên myController thuộc ứng dụng myApp, thực hiện chức năng đơn giản là tăng giá trị cho biến counter mỗi khi người dùng bấm vào nút lệnh Increment:
Ví dụ minh họa về Controller
//----------------------------------------------------------------------
// file: example-karma-unit-testing-controller/main/js/myController.js
//
var app = angular.module('myApp', []);

app.controller('myController', ['$scope', 'backendService',
function($ctrlScope, $backend) {
$ctrlScope.counter = 0;

$ctrlScope.incrementCounter = function() {
$ctrlScope.counter += $backend.step();
}

$ctrlScope.resetCounter = function() {
$ctrlScope.counter = $backend.init();
}
}]);
Lưu ý myController có sử dụng một Service có tên là backendService, Service này vẫn chưa được lập trình. Khi đó, ta cần giả lập backendService khi lập trình Spec kiểm thử myController.

Tạo Spec kiểm thử Controller

Tạo một Spec Suite kiểm thử cho myController, định nghĩa trong tệp myController.Spec.js như sau:
//----------------------------------------------------------------------
// file: example-karma-unit-testing-controller/test/unit/myController.Spec.js
//
describe("myController Unit testing #1", function () {

// Arrange
var mockScope;
var controller;

beforeEach(angular.mock.module("myApp"));

beforeEach(angular.mock.inject(function ($controller, $rootScope) {
mockScope = $rootScope.$new();

controller = $controller("myController", {
$scope: mockScope,
backendService: {
init: function() {
return 1;
},
step: function() {
return 5;
},
echo: function(msg) {
return 'echo[' + msg + ']';
}
}
});
}));

it("Creates variable", function () {
expect(mockScope.counter).toEqual(0);
})

it("Increments counter", function () {
mockScope.incrementCounter();
expect(mockScope.counter).toEqual(5);
});

it("Resets counter", function () {
mockScope.resetCounter();
expect(mockScope.counter).toEqual(1);
});
});
Lưu ý Service backendService được giả lập trực tiếp bằng cách định nghĩa một đối tượng cụ thể và gán cho thuộc tính backendService trong quá trình tạo myController.

Cải tiến cách kiểm thử

Chúng ta có thể cải tiến cách giả lập Service backendService bằng cách sử dụng các hàm spyOn() của Jasmine để định nghĩa các hàm của Service backendService ngay trong từng hàm kiểm thử (Spec).
//----------------------------------------------------------------------
// file: example-karma-unit-testing-controller/test/unit/myController.Spec.js
//
describe("myController Unit testing #2", function () {

// Arrange
var mockScope;
var mockBackendService;
var controller;

beforeEach(angular.mock.module("myApp"));

beforeEach(angular.mock.inject(function ($controller, $rootScope) {
mockScope = $rootScope.$new();

mockBackendService = {
init: function() {},
step: function() {}
};

controller = $controller("myController", {
$scope: mockScope,
backendService: mockBackendService
});
}));

it("Creates variable", function () {
expect(mockScope.counter).toEqual(0);
})

it("Increments counter", function () {
spyOn(mockBackendService, 'step').and.callFake(function() {
return 5;
});

mockScope.incrementCounter();

expect(mockScope.counter).toEqual(5);
});

it("Resets counter", function () {
spyOn(mockBackendService, 'init').and.callFake(function() {
return 1;
});

mockScope.resetCounter();

expect(mockBackendService.init).toHaveBeenCalled();
expect(mockScope.counter).toEqual(1);
});
});

Áp dụng Unit Testing cho các Service

Tải ví dụ minh họa

Tải ví dụ minh họa “AngularJS Service Unit Testing” theo địa chỉ dành cho git clone hoặc bản nén zip. Sau khi tải xong ví dụ, chuyển vào thư mục example-karma-unit-testing-service, thực hiện lệnh sau để cài đặt karma:
$ npm install karma karma-jasmine karma-chrome-launcher --save-dev
Sau khi npm cài đặt xong các gói thư viện, chạy thử ví dụ bằng lệnh:
$ karma start karma-unit.conf.js

Mô tả Service cần kiểm thử

Tương tự như với myControler, chúng ta cũng xem xét một Service đơn giản có tên là myService, có chức năng đơn giản như sau:
//----------------------------------------------------------------------
// file: example-karma-unit-testing-service/main/js/myService.js
//
var app = angular.module('myApp', []);

app.factory('myService', ['backendService', function($backend) {
return {
test: function(msg) {
return "Returned message:" + $backend.echo(msg);
}
};
}]);
Trong đoạn mã lệnh trên, myService sử dụng hàm echo() của một Service chưa tồn tại có tên là backendService. Để kiểm thử myService (trong ví dụ này, myService chỉ có một hàm là test()), chúng ta sẽ giả lập Service backendService

Tạo Spec kiểm thử Service

//----------------------------------------------------------------------
// file: example-karma-unit-testing-service/test/unit/myService.Spec.js
//
describe('myService Unit Testing', function (){

var myService, mockBackend;

beforeEach(angular.mock.module('myApp', function($provide) {
mockBackend = jasmine.createSpyObj('backendService', ['echo']);
$provide.value('backendService', mockBackend);
}));

beforeEach(angular.mock.inject(function(_myService_) {
myService = _myService_;
}));

it('should call backendService.echo on myService.test', function () {
mockBackend.echo.and.callFake(function(msg) {
return 'echo[' + msg + ']';
});

// make the call.
result = myService.test('Hello');

// check our spy to see if echo was called properly.
expect(mockBackend.echo).toHaveBeenCalledWith('Hello');
expect(result).toEqual("Returned message:echo[Hello]");
});
});

Áp dụng Unit Testing cho các Filter

Tải ví dụ minh họa

Tải ví dụ minh họa “AngularJS Filter Unit Testing” theo địa chỉ dành cho git clone hoặc bản nén zip. Sau khi tải xong ví dụ, chuyển vào thư mục example-karma-unit-testing-filter, thực hiện lệnh sau để cài đặt karma:
$ npm install karma karma-jasmine karma-chrome-launcher --save-dev
Sau khi npm cài đặt xong các gói thư viện, chạy thử ví dụ bằng lệnh:
$ karma start karma-unit.conf.js

Mô tả Filter cần kiểm thử

//----------------------------------------------------------------------
// file: example-karma-unit-testing-filter/main/js/myFilter.js
//
var app = angular.module('myApp', []);

app.filter('myFilter', function() {
return function(value, number) {
if (angular.isString(value)) {
number = angular.isNumber(number) ? number : value.length;
if (number < 0) return value;
if (number >= value.length) return value.toUpperCase();
var sub = value.substr(0, number);
return sub.toUpperCase() + value.substr(number);
} else {
return value;
}
};
});

Tạo Spec kiểm thử Filter

//----------------------------------------------------------------------
// file: example-karma-unit-testing-filter/test/unit/myFilter.Spec.js
//
describe('myFilter Unit Testing', function (){

var filterInstance;
beforeEach(angular.mock.module("myApp"));

beforeEach(angular.mock.inject(function ($filter) {
filterInstance = $filter("myFilter");
}));

it("Uppercase full length of string", function () {
var result = filterInstance("testing filter");
expect(result).toEqual("TESTING FILTER");
});

it("Uppercase first five characters of string", function () {
var result = filterInstance("testing filter", 5);
expect(result).toEqual("TESTIng filter");
});
});

Áp dụng Unit Testing cho các Directive

Tải ví dụ minh họa

Tải ví dụ minh họa “AngularJS Directive Unit Testing” theo địa chỉ dành cho git clone hoặc bản nén zip. Sau khi tải xong ví dụ, chuyển vào thư mục example-karma-unit-testing-directive, thực hiện lệnh sau để cài đặt karma:
$ npm install karma karma-jasmine karma-chrome-launcher --save-dev
Sau khi npm cài đặt xong các gói thư viện, chạy thử ví dụ bằng lệnh:
$ karma start karma-unit.conf.js

Mô tả Directive cần kiểm thử

//----------------------------------------------------------------------
// file: example-karma-unit-testing-directive/main/js/myDirective.js
//
var app = angular.module('myApp', []);

app.directive('myDirective', function($timeout) {
var html =
'<div class="notification">' +
'<div class="notification-content">' +
'<p></p>' +
'</div>' +
'</div>';
return {
restrict: 'E',
scope: { message: '=' },
template: html,
replace: true
}
});

Tạo Spec kiểm thử Directive

//----------------------------------------------------------------------
// file: example-karma-unit-testing-directive/test/unit/myDirective.Spec.js
//
describe('myDirective Unit Testing', function() {
var mockElement;
var mockScope;

beforeEach(angular.mock.module('myApp'));

beforeEach(angular.mock.inject(function($compile, $rootScope) {
mockScope = $rootScope.$new();
mockElement = angular.element(
'<my-directive message="note"></my-directive>');
$compile(mockElement)(mockScope);
mockScope.$apply();
}));

it('should display the welcome text', function() {
mockScope.$apply(function() {
mockScope.note = "Welcome to Danang Javascript";
});
expect(mockElement.html()).toContain("Welcome to Danang Javascript");
});
});




Klik untuk melihat kode: :) =( :s :D :-D ^:D ^o^ 7:( :Q :p T_T @@, :-a :W *fck* x@