Testing cho Directive với Jasmine

Testing cho Directive với Jasmine

1. Đặt vấn đề

Trong quá trình làm việc, Tôi gặp phải không ít các ticket liên quan đến Angularjs và để đảm bảo chất lượng đôi khi tôi phải viết test case cho các function của mình. Bài viết sau đây tôi sẽ chia sẻ những kiến thức mà tôi học được khi viết test cho một directive. Hi vọng nó sẽ hữu ích với bạn.

Nếu các bạn đã viết test cho Angularjs thì các bạn sẽ thấy việc viết test cho Directive có chút khó khăn hơn so với test cho controller hoặc services. Đối với Directive chúng ta cần render ra một tempate. và xác định được chính xác element mà chúng ta sẽ render trên template. Điều này sẽ được thực hiện thông qua đối tượng $complile.

Bài viết này tôi sẽ tập trung nói về việc viết test cho một driective đơn giản và test cho một function thuộc controller nằm trong directive.

Trước tiên tôi sẽ tạo ra một Directive và sử dụng nó xuyên xuốt bài viết để viết test cho nó.

Hãy tạo ra một directive đơn giản để liệt kê ra các ngôn ngữ lập trình

  • scritp.js

angular.module('language', []);

var Module = angular.module('language');

Module.controller('languageController', ['$scope',
  function(scope) {
    scope.items = [{
      id: 0,
      title: 'ROR'
    }, {
      id: 1,
      title: 'PHP'
    }, {
      id: 2,
      title: 'JAVA'
    }, {
      id: 3,
      title: 'IOS'
    }];
  }
]);

Module.directive('displayedItems', function() {
  return {
    restrict: 'EA',
    transclude: true,
    replace: true,
    scope: {
      items: '=items'
    },
    controller: function ($scope) {
      var items = $scope.items || [];
  init();
  function init() {
    if ($scope.items.length > 0) $scope.items[0].selected = true;
  }

  $scope.selectItem = function(item) {
    angular.forEach(items, function(item) {
      item.selected = false;
    });
    item.selected = true;
  };
 },
    template: '<div class="table table">' +
      '<div ng-transclude></div>' +
      '<div ng-repeat="item in items">' +
      '<div class="item-class" ng-class="{active: item.selected}">{{ item.title }} - id: {{ item.id }}' +
      ' <button class="btn" ng-disabled="item.selected" ng-click="selectItem(item)">Chọn</button>' +
      '</div>' +
      '</div>'
  };
});
  • index.html
<!DOCTYPE html>
<html>
<body>
  <h1>Testing cho Directive với Jasmine</h1>
  <div ng-app="language">
    <div ng-controller="languageController">
      <displayed_items data-items="items">
        <b>Chào Hoang Quan, dưới đây là một trong những ngôn ngữ lập trình. Hãy chọn  ngôn ngữ lập trình của bạn</b>
      </displayed_items>
    </div>
  </div>
</body>
</html>

Ứng dụng đơn giản trên dùng để hiển thị ra danh sách các ngôn ngữ lập trình và chọn ngôn ngữ lập trình của bạn.

alt

Bây giờ hãy tiến hành viết test cho directive code js trong đoạn ứng dụng trên nhé.

2. Testing cho một directive tamplates với Jasmine

Nhìn vào ví dụ bạn có thể thấy về cơ bản ứng dụng này sẽ render ra một list các items nằm trong các thẻ div. để xác nhận được điều này ta sẽ viết test để chứng thực được nội dung hiển thị sẽ chứa các thông tin mà ta mong muốn thông qua các element có trên view.

Trước tiên, Tôi sẽ chứng thực rằng directive của ta sẽ render ra các items cụ thể là 2 items và trên các items có chứa nội dung là các ngôn ngữ lập trình mà ta định nghĩa.

Hãy thực hiện đoạn code sau:


describe('Directive', function() {
  var elm, scope;
  beforeEach(module('language'));
  beforeEach(inject(function($rootScope, $compile) {
    elm = angular.element(
      '<div>' +
      '{{ customMessage }}' +
      '<displayed-items data-items="items">' +
      '</displayed-items>' +
      '</div>');
    scope = $rootScope.$new();
    scope.customMessage = '<div class="custom-message">Tiêu đề</div>';
    scope.items = [{
      id: 1,
      title: 'Ngôn ngữ lập trình A'
    }, {
      id: 2,
      title: 'Ngôn ngữ lập trình B'
    }];
    $compile(elm)(scope);
    scope.$digest();
  }));
  it('should  have items and contains Langguas ', function() {
    var items = elm.find('.item-class');
    expect(items.length).toBe(1);
    expect(items.eq(0).text()).toContain('title a');
    expect(items.eq(1).text()).toContain('title b');
  });
});

Như bạn đã thấy để test cho bất ký diredtive hay controller nào ta phải khai báo module mà ta sẽ gọi:

beforeEach(module('language'));

Tiếp theo, Không giống như test cho Controller hay service. Để test cho một diiective bạn sẽ phải khai báo element được render ra trên view:


elm = angular.element(
      '<div>' +
      '{{ customMessage }}' +
      '<displayed-items data-items="items">' +
      '</displayed-items>' +
      '</div>');

Sau đó khai báo scope và gán giá trị cho scope đó:

    scope = $rootScope.$new();
    scope.customMessage = '<div class="custom-message">Tiêu đề</div>';
    scope.items = [{
      id: 1,
      title: 'Ngôn ngữ lập trình A'
    }, {
      id: 2,
      title: 'Ngôn ngữ lập trình B'
    }];

Như đã nói ở trên, Chúng ta sẽ không thực hiện render ra view thật, nên chúng ta cần sử dụng biến $compile để biên dịch element tthành html và $digest để update data từ scope khai báo:

$compile(elm)(scope);
scope.$digest();

Chạy test case trên ta sẽ thấy kết quả faile:

alt

Sửa dữ liệu cho đúng ta sẽ pass được test case này:

expect(items.length).toBe(2);
expect(items.eq(0).text()).toContain('Ngôn Ngữ Lập Trình A');
expect(items.eq(1).text()).toContain('Ngôn Ngữ Lập Trình B');

alt

3. Testing cho một function thuộc controller trong directives

Như bạn đã biết, Trong Angularjs chúng ta thường viết controller tách biệt hoàn toàn với directives và gọi nó trong directive bằng tên của Controller. Tuy nhiên trong thực tế cũng có lúc ta viết controller ngay trong directive mà không cần khai báo tên. Tôi đã gặp trường hợp này trong thực tế dự án của mình.

Trong trường hợp này, tôi đã sử dụng element để gọi đến phương thức selectItem được định nghĩa trong controller nằm trong directive displayedItems.

  $scope.selectItem = function(item) {
    angular.forEach(items, function(item) {
      item.selected = false;
    });
    item.selected = true;
  };

Phương thức selectItem nhằm mục đích disable đi button Chọn khi bạn click vào button này, đồng thời hiện enable các button khác được chọn trước đó:

  • Sẽ disable hoặc enable khi click button "Chọn"
   it('should disable/enable when click to button ', function() {
    var items = elm.find('.item-class');
    expect(items.eq(1).find('button').attr('disabled')).toBeUndefined();
    items.eq(1).find('button').click();
    expect(items.eq(1).find('button').attr('disabled')).toBeTruthy();
  });

Kết quả:

alt

4. Mock dữ liệu như thế nào

Trong ví dụ đầu tiên, bạn có thể thấy tôi đã tạo dữ liệu giả cho list các items của mình bằng cách gán vào scope như sau:

scope.items = [{
      id: 1,
      title: 'Ngôn ngữ lập trình A'
    }, {
      id: 2,
      title: 'Ngôn ngữ lập trình B'
    }];

Tuy nhiên nếu dữ liệu mà bạn cần lớn và phức tạp hơn thế thì việc viết nó ra trong hàm test sẽ làm cho test file của bạn lớn và khó kiển tra được code. Đó là lý do tôi dùng thư viện mocks để mocks một data phức tạp.

  • cài đặt
npm install angular-mocks

config cho karma nhận thư viện mới này với tên biến xác định:

module.exports = function(config) {
    config.set({
	jsonFixturesPreprocessor: {
         	 // change the global fixtures variable name
          	  variableName: 'mocks'
        	},
     }
}

Trong trường hợp này dữ liệu của tôi là json nên tôi cài đặt thêm thư viện

sudo npm install karma-json-fixtures-preprocessor

để mocks sẽ làm việc được với file json

	preprocessors: {
          'mocks/*.json': ['json_fixtures']
    },

Tạo ra file test.json làm dữ liệu mocks với nội dung như sau:

	{
   "data": {
		"items": [
			{id: 1,  title: 'Ngôn ngữ lập trình A'},
			{id: 2,  title: 'Ngôn ngữ lập trình B'},
			];
		}
    }

Cuối cùng gọi nó trong test case của mình bằng cách:

scope.items = mocks['mocks/test'].data.items;

Tham khảo sources code tại Đây

Thanks


All Rights Reserved