Сегодняшнею статью я решил посвятить покрытию unit-тестами вашего AngularJS приложения с Jasmine, а также тому, как отлаживать этот проект с Visual Studio. Так как на это
все я потратил достаточно долго времени, решил поделиться с вами тем, что у меня
получилось. Отправной точкой начала работы для меня стала
статья To Unit Test AngularJS with Jasmine in
Visual Studio, благодаря которой
я смог настроить дебаг тестов с Visual Studio. Также я подружил Resharper 9 с этими тестами.
Так что весь этот процесс мы с вами сейчас и рассмотрим.
1.Первым делом устанавливаем с помощью Visual Studio Extension компонент Chutzpah – это open source Unit test runner. Для этого
переходим в меню Tools->Extensions and Updates…
Затем открываем Online->Visual Studio Gallery и ищем в поиске по
слову Chutzpah.
Для студии я себе ставил только
екстеншн, который позволяет запускать с контекстного меню. Вы же можете
поставить себе оба расширения. Мне для отладки приложения полностью хватило
только одного расширения по той причине, что я работаю с тестами через Resharper. Для того чтобы это работало
в Test Explorer, ставьте себе первый в списке Chutzpah test-адаптер.
2. Нам нужно создать пустое ASP.NET Web Application (Empty), которое назовем AngulaJsUnitTestSample.
На следующем этапе выбираем просто Empty шаблон и нажимаем
на кнопку OK.
3. С помощью NuGet Package Manager ставим себе AngularJS.Core.
Либо с помощью Package Manager
Console (ALT+T, N, O):
install-package
AngularJS.Core
4. Поставим через NuGet Package Manager еще и Jasmine test-фреймворк.
Либо то же самое через PMC (Package Manager Console)
install-package JasmineTest
Затем удаляем с проекта выбранные файлы, как
показано на рисунке ниже.
5. Создаем фейковый тест, чтобы
проверить работоспособность. Для этого создадим в корне папку Tests, в которую добавим
файл JavaScript файл, который
назовем jasmineWorks.js, и добавим в него
код, показанный ниже.
describe('jasmine works', function () {
it('sanity check', function () {
expect(0).toBe(0);
});
});
После этого, если вы ставили себе адаптер, вы
можете запустить ваш тест с Test Explorer (если он не
открыт, то выбираем Test -> Windows
-> Test Explorer).
И проверяем, что тест успешно отработал.
Можно это тест запустить с кода, используя
расширения для контекстного меню. Тогда у вас будет показан результат в виде симпатичной веб-страницы. Для этого поставим курсор в наш тест, нажмем правую клавишу
мыши, и в контекстном меню выберем пункт Run JS Tests, если просто хотим собрать тесты и проверить, что они
проходят.
Либо выбрать Open in browser, чтобы посмотреть
на результат.
Опции Run Chutzpah With позволяют
запускать тесты в режиме отладки и посмотреть покрытие кода тестами.
Ниже показано запуск в режиме отладки.
И также покрытие тестами.
6. Добавить просто AngularJS контроллер.
В папку Scripts добавим новый JavaScript файл, который назовем appController.js, реализация
которого приведена ниже.
angular.module('app', []).controller('appController', function ($scope) {
$scope.value = 5;
});
7. Добавить unit test для appController.
Перейдем в папку Tests и добавим туда новый JavaScript файл, который
назовем appControllerSpec.js. Ниже приведена
его реализация.
/// <reference path="../Scripts/angular.js" />
/// <reference path="../Scripts/angular-mocks.js" />
/// <reference path="../Scripts/appController.js" />
describe('When using appController ', function () {
//initialize Angular
beforeEach(module('app'));
//parse out the scope for use in our unit tests.
var scope;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
var
ctrl = $controller('appController', { $scope: scope });
}));
it('initial value is 5', function () {
expect(scope.value).toBe(5);
});
});
8. Запускаем наш тест.
Это все хорошо для простых проектов, у
которых нет никаких зависимостей и т.д. Но что делать, если у нас проект
посложнее, в котором есть работа с сетью, есть модули, в которых прописана
маршрутизация, и контроллеры принимают множество параметров. Поэтому мы с вас
попробуем на уже существующем тестовом проекте для работы со студентами покрыть тестами хотя бы один контроллер. Но для начала я покажу, как настроить Resharper, для того чтобы не
запускать тесты через неудобный Test Explorer, который все ваши
тесты показывает одним списком, смешивая в кучу все Unit/Integration тесты. JavaScript тесты через
решарпер запускаем через WebKit, который называется PhantomJS. Загружаем себе файл phantomjs.exe
(настройки актуальны для Windows) по ссылке выше.
Затем открываем Resharper ->Options и ищем в самом
низу Tools. Затем в Tools выбираем пункт Unit Testing -> JavaScript Tests.
Теперь по самим настройкам. Первое: ставим
галочку Enable Jasmine support и выбираем с выпадающего пункта Jasmine version версию 2.0. После
этого нужно также выбрать пункт PhantomJS с указанием пути к
папке, в которую вы скачали ваш PhantomJS.exe. На рисунке ниже показано, как все нужно
сделать.
Сохраняем и проверяем, что все работает. Для
этого выбираем наш Solution и запускам наши тесты с меню решарпера.
Или быстрой комбинацией Ctrl+T, R. Ниже показан
результат запуска через Resharper этих же тестов.
Теперь давайте усложним пример, как я писал
выше. У нас для примера есть контроллер, который называется studentsCtr. Ниже приведена его реализация.
(function () {
'use strict';
angular
.module('app')
.controller('studentsCtr', studentsCtr);
studentsCtr.$inject = ['$scope', '$http', '$location'];
function studentsCtr($scope,
$http, $location) {
console.log("studentsCtr");
var
url = 'api/Students';
$http.get(url)
.success(function (data) {
$scope.students = data;
})
.error(function (error) {
console.log(error.message);
});
$scope.addNewStudent = function () {
$location.path('/create');
};
$scope.editRow = function (entity) {
$location.path('/edit/' + entity.Id);
};
$scope.removeRow = function (index, entity) {
$http.delete('api/Students/' + entity.Id)
.success(function () {
$scope.students.splice(index,
1);
})
.error(function (error) {
console.log(error.Message);
});
};
}
})();
Нам в целом не важно, как это все
реализовано на web-странице, нам это просто не интересно. Нас может, разве что, заинтересовать, как построен сам модуль app.js.
(function () {
'use strict';
angular.module('app',
[
// Angular modules
'ngRoute',
'ui.bootstrap',
'ui.bootstrap.tpls'
// Custom modules
// 3rd Party Modules
])
.config(function ($routeProvider, $locationProvider) {
console.log("app");
$routeProvider
.when('/',
{
templateUrl: 'Views/appview/studentsview.html',
controller: 'studentsCtr'
})
.when('/create', {
templateUrl: 'Views/appview/createstudent.html',
controller: 'createStudentCtr'
})
.when('/edit/:studentId', {
templateUrl: 'Views/appview/createstudent.html',
controller: 'editStudentCtr'
})
.otherwise({
redirectTo: '/'
});
$locationProvider.html5Mode(true);
});
})();
Все этим примеры взяты со статьи. Исходники
на скачивание этих примеров будут доступны в конце статьи.
Вернемся теперь к нашему контроллеру studentCtr. Как вы можете
заметить, первое действие, которое выполняется здесь, – это с помощью $http контекста мы
загружаем данные по студентам с какого-то Web API контроллера. На
всякий случай, ниже приведена
реализация этого метода.
private StudentContext db =
new StudentContext();
// GET:
api/Students
public IQueryable<Student> GetStudents()
{
return db.Students;
}
В данном случае нам даже не важно, как
реализован класс Student. Нам важно, чтобы после того, как отработал метод,
var url =
'api/Students';
$http.get(url)
.success(function (data) {
$scope.students = data;
})
.error(function (error) {
console.log(error.message);
});
в $scope.students появились нужные нам данные. Для этого нужно замокать $http наш контекст.
Делается это очень просто: в angular-mocks.js есть фейковая реализация $httpBackend, которая заменяет
реальную реализацию $httpBackend. Важное замечание: $httpBackend – это не то же самое, что и $http сервис. $http сервис использует $httpBackend для отправки HTTP-сообщений.
Так как теорию мы рассмотрели и, по сути, поняли, что нам нужно мокать, настало самое время приступить к реализации. Для
этого нам нужно повторить все действия с данной статьи с пункта 1 по 8-й.
Затем, когда у нас будет установлен JasmineTest, можно приступать к самой реализации. Для
этого в папку Tests (если этой папки
еще нет, то нужно ее создать) добавляю новый JavaScript файл с названием studentsCtrSpec.js. Ниже
приведена полная реализация этого файла.
/// <reference path="../Scripts/angular.js" />
/// <reference path="../Scripts/angular-route.js" />
/// <reference path="../Scripts/angular-ui/ui-bootstrap.js"
/>
/// <reference
path="../Scripts/angular-ui/ui-bootstrap-tpls.js" />
/// <reference path="../Scripts/angular-mocks.js" />
/// <reference path="../Scripts/app/app.js" />
/// <reference path="../Scripts/app/controllers/studentsCtr.js"
/>
describe('When using studentsCtr ', function () {
beforeEach(module('app'));
describe("studentsCtr", function () {
var
scope, httpBackend;
beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http, $location) {
scope = $rootScope.$new();
httpBackend = $httpBackend;
httpBackend.when("GET", "api/Students").respond([{}, {}, {}]);
$controller('studentsCtr', {
$scope: scope,
$http: $http,
$location: $location
});
}));
it('should GET studentsCtr', function () {
httpBackend.flush();
expect(scope.students.length).toBe(3);
});
});
});
С самого начала я добавил ссылки на все зависимости,
которые есть в проекте. Без них ваш код не заработает. Сейчас я постараюсь
детально объяснить почему. Первой строчкой у нас идет вызов такого кода:
beforeEach(module('app'));
Мы пытаемся подгрузить наш app-модуль. Но здесь
есть основная проблема. В этом модуле прописано куча зависимостей, без которых
это все не будет работать.
angular.module('app', [
// Angular modules
'ngRoute',
'ui.bootstrap',
'ui.bootstrap.tpls'
// Custom modules
// 3rd Party Modules
])
Как видите, у нас зависимости от следующих
частей:
- ngRoute – файл angular-route.js
- ui.bootstrap – ui-bootstrap.js
- ui.bootstrap.tpls – ui-bootstrap-tpls.js
Теперь вы поняли, почему мы добавили
загрузку этих всех зависимостей. Для более сложных проектов эти зависимости
можно добавлять в отдельный файл _references.js и использовать уже только его.
Как видите, зависимостей, которые нужно
подгружать, достаточно много. И чем их больше, тем сложнее писать нам свои
тесты. Ниже приведен код, который может быть и который нужно покрыть тестами.
angular.module("app",
['ngRoute', 'ngTouch', 'ui.grid', 'ui.grid.edit', 'ui.grid.rowEdit',
'ui.grid.cellNav', 'ui.grid.pagination', 'ui.grid.resizeColumns', 'ui.bootstrap', "ui.bootstrap.tpls", "ui.grid.grouping",'ui.grid.expandable', 'ui.grid.selection', 'ui.grid.pinning', "ngAnimate",
"crumble", 'angular-loading-bar', 'ng.deviceDetector','ui-notification',"xeditable"])
Как видите, зависимостей очень много. Так
что, если у вас такое на проекте, выносите сразу все в отдельный файл.
Следующий код также не очень сложный. В нем
мы создаем новый контекст, и если у нас вызывается наш запрос, то возвращаем
три пустых объекта, что будет равно трем студентам для нашего списка.
beforeEach(inject(function ($rootScope, $controller, $httpBackend, $http, $location) {
scope = $rootScope.$new();
httpBackend = $httpBackend;
httpBackend.when("GET", "api/Students").respond([{}, {}, {}]);
$controller('studentsCtr', {
$scope: scope,
$http: $http,
$location: $location
});
}));
И в самом конце – проверка, что все вызвалось
успешно.
it('should GET studentsCtr', function () {
httpBackend.flush();
expect(scope.students.length).toBe(3);
});
Метод flush() также проводит проверку, что никаких лишних
запросов, кроме наших, не было вызвано. Проверка на количество полученных
объектов также в комментировании не нуждается. Ниже показано, как это выглядит в
дебаг-режиме.
Если вы запустите в дебаге через Chutzpah, то получите
следующий результат:
Я же предпочитаю запуск через Resharper.
Итоги
Думаю, на этой позитивной ноте мы можем заканчивать. Надеюсь, что если вы только начинаете с разбираться с этой темой, сегодняшний материал поможет вам
сэкономить время и позволит вам более легко погрузиться в
мир тестирования вашего AngularJS и JavaScript кода в ваших проектах.