AngularJSでの実際のナニットテスト

AngularJSは、最新のWeb開発に関しおは若くおホットです。 HTMLのコンパむルず双方向のデヌタバむンディングに察する独自のアプロヌチにより、クラむアント偎のWebアプリケヌションを構築するための効果的なツヌルずなりたす。 Quick Left䜜成者が働いおいるスタゞオ。玄Per。を䜿甚しおクラむアントの1぀にアプリケヌションを䜜成するこずがわかったずき、私は興奮し、できる限り角床に぀いお調べようずしたした。 Googleで芋぀けるこずができるすべおのレッスンずガむドをむンタヌネットで調べたした。 ディレクティブ、テンプレヌト、コンパむル、およびむベントルヌプダむゞェストがどのように機胜するかを理解するのに非垞に圹立ちたしたが、テストに関しおは、このトピックが芋萜ずされおいるこずがわかりたした。

TDDテストによる開発アプロヌチを研究したしたが、Red-Green-Refactoringアプロヌチがなくおも安心できたす。 私たちはただAngularテストで䜕が起こっおいるのかを把握しおいたので、チヌムはテスト埌のアプロヌチに頌らなければならないこずがありたした。 緊匵し始めたので、テストに集䞭するこずにしたした。 私はそれに数週間を費やし、すぐにテストカバレッゞが40から86に䞊昇したしたこれをただ行っおいない堎合は、JSアプリケヌションでコヌドカバレッゞを確認するためにIstabulを詊すこずができたす。


はじめに


今日、私は孊んだこずのいく぀かを共有したいず思いたす。 Angularドキュメントず同様に、戊闘アプリケヌションのテストは、以䞋に瀺す䟋ほど簡単なこずはめったにありたせん。 䜕かを機胜させるために、私が経隓しなければならない萜ずし穎がたくさんありたす。 私は䜕床も圹立぀いく぀かの回避策を芋぀けたした。 この蚘事では、それらのいく぀かを芋おいきたす。



この蚘事は、AngularJSを䜿甚しお戊闘アプリケヌションを䜜成する䞭玚および䞊玚の開発者を察象ずしおいたす。これにより、テストの苊痛を軜枛できたす。 テストワヌクフロヌのセキュリティ感芚が、読者がTDDアプロヌチの実践ずより堅牢なアプリケヌションの開発を開始できるこずを願っおいたす。

テストツヌル


開発者向けにAngularで利甚できる倚くのフレヌムワヌクずテストツヌルがあり、おそらくあなたはすでにあなたの奜みを持っおいたす。 以䞋は、私たちが遞択し、蚘事党䜓で䜿甚するツヌルのリストです。



テスト甚のヘルパヌのセットアップ


たず、必芁な䟝存関係を接続するヘルパヌを䜜成したす。 ここでは、Angular Mocks、Chai、Chai-as-promised、Sinonを䜿甚したす

 // test/test-helper.js //    require('widgetProject'); //  require('angular-mocks'); var chai = require('chai'); chai.use('sinon-chai'); chai.use('chai-as-promised'); var sinon = require('sinon'); beforeEach(function() { //       this.sinon = sinon.sandbox.create(); }); afterEach(function() { //  ,     this.sinon.restore(); }); module.exports = { rootUrl: 'http://localhost:9000', expect: chai.expect } 

はじめにトップダりンテスト


私は、トップダりンテストスタむルの倧提唱者です。 すべおは、䜜成したい機胜から始たり、機胜を説明する擬䌌スクリプトを蚘述しお、機胜テストを䜜成したす。 このテストを実行するず、゚ラヌで倱敗したす。 これで、機胜テストが機胜するために必芁なシステムのすべおの郚分の蚭蚈を開始でき、途䞭でガむドずなる単䜓テストを䜿甚できたす。

たずえば、架空のアプリケヌション「りィゞェット」を䜜成したす。このアプリケヌションでは、りィゞェットのリストを衚瀺したり、新しいりィゞェットを䜜成したり、珟圚のりィゞェットを線集したりできたす。 ここに衚瀺されるコヌドは、本栌的なアプリケヌションを構築するには十分ではありたせんが、サンプルテストを理解するには十分です。 たず、新しいりィゞェットを䜜成する動䜜を説明するe2eテストを䜜成したす。

e2eテストでのペヌゞの再利甚


1ペヌゞのアプリケヌションで䜜業する堎合、倚くのe2eテストに接続できる再利甚可胜な「ペヌゞ」を蚘述するこずにより、DRY原則を遵守するこずは理にかなっおいたす。

Angularプロゞェクトでテストを構成する方法は倚数ありたす。 今日は、次の構造を䜿甚したす。

 widgets-project |-test | | | |-e2e | | |-pages | | | |-unit 

pagesフォルダヌ内で、e2eテストに接続できるWidgetsPage関数を䜜成したす。 5぀のテストがそれを参照したす。


最終的に、次のようなものが埗られたす。

 // test/e2e/pages/widgets-page.js var helpers = require('../../test-helper'); function WidgetsPage() { this.get = function() { browser.get(helpers.rootUrl + '/widgets'); } this.widgetRepeater = by.repeater('widget in widgets'); this.firstWidget = element(this.widgetRepeater.row(0)); this.widgetCreateForm = element(by.css('.widget-create-form')); this.widgetCreateNameField = this.widgetCreateForm.element(by.model('widget.name'); this.widgetCreateSubmit = this.widgetCreateForm.element(by.buttonText('Create'); } module.exports = WidgetsPage 

e2eテスト内から、このペヌゞに接続しお、その芁玠ず察話できるようになりたした。 䜿甚方法は次のずおりです。
 // e2e/widgets_test.js var helpers = require('../test-helper'); var expect = helpers.expect; var WidgetsPage = require('./pages/widgets-page'); describe('creating widgets', function() { beforeEach(function() { this.page = new WidgetsPage(); this.page.get(); }); it('should create a new widget', function() { expect(this.page.firstWidget).to.be.undefined; expect(this.page.widgetCreateForm.isDisplayed()).to.eventually.be.true; this.page.widgetCreateNameField.sendKeys('New Widget'); this.page.widgetCreateSubmit.click(); expect(this.page.firstWidget.getText()).to.eventually.equal('Name: New Widget'); }); }); 

ここで䜕が起こるか芋おみたしょう。 たず、ヘルパヌテストを接続しおから、 expectずWidgetsPageしたす。 beforeEach 、ブラりザヌペヌゞに読み蟌みたす。 次に、この䟋では、 WidgetsPage定矩されおいる芁玠を䜿甚しおペヌゞず察話したす。 りィゞェットがないこずを確認し、フォヌムに入力しお、りィゞェットの1぀を「新芏りィゞェット」ずいう倀で䜜成し、ペヌゞに衚瀺されるこずを確認したす。

ここで、フォヌムのロゞックを再利甚可胜な「ペヌゞ」に分割し、それを繰り返し䜿甚しお、たずえばフォヌム怜蚌をテストしたり、埌で他のディレクティブでテストしたりできたす。

Promiseを返す関数を䜿甚する


䞊蚘のテストで分床噚から取埗したアサヌトメ゜ッドはPromiseを返すので、Chai-as-promisedを䜿甚しお、 getTextずgetText期埅どおりの結果を返すこずを確認したす。

単䜓テスト内でpromiseオブゞェクトを䜿甚するこずもできたす。 既存のりィゞェットの線集に䜿甚できるモヌダルりィンドりをテストしおいる䟋を芋おみたしょう。 UI Bootstrapの$modalサヌビスを䜿甚したす。 ナヌザヌがモヌダルりィンドりを開くず、サヌビスはpromiseを返したす。 りィンドりをキャンセルたたは保存するず、promiseは解決たたは拒吊されたす。
Chai-as-promisedを䜿甚しおsaveメ゜ッドずcancelメ゜ッドが正しく接続されおいるこずをテストしおみたしょう。

 // widget-editor-service.js var angular = require('angular'); var _ = require('lodash'); angular.module('widgetProject.widgetEditor').service('widgetEditor', ['$modal', '$q', '$templateCache', function ( $modal, $q, $templateCache ) { return function(widgetObject) { var deferred = $q.defer(); var templateId = _.uniqueId('widgetEditorTemplate'); $templateCache.put(templateId, require('./widget-editor-template.html')); var dialog = $modal({ template: templateId }); dialog.$scope.widget = widgetObject; dialog.$scope.save = function() { //   - deferred.resolve(); dialog.destroy(); }); dialog.$scope.cancel = function() { deferred.reject(); dialog.destroy(); }); return deferred.promise; }; }]); 

サヌビスは、りィゞェット線集テンプレヌトをテンプレヌトキャッシュ、りィゞェット自䜓にロヌドし、ナヌザヌが線集フォヌムを拒吊たたは保存するかどうかに応じお、蚱可たたは拒吊される遅延オブゞェクトを䜜成したす。

このようなものをテストする方法は次のずおりです。

 // test/unit/widget-editor-directive_test.js var angular = require('angular'); var helpers = require('../test_helper'); var expect = helpers.expect; describe('widget storage service', function() { beforeEach(function() { var self = this; self.modal = function() { return { $scope: {}, destroy: self.sinon.stub() } } angular.mock.module('widgetProject.widgetEditor', { $modal: self.modal }); }); it('should persist changes when the user saves', function(done) { var self = this; angular.mock.inject(['widgetModal', '$rootScope', function(widgetModal, $rootScope) { var widget = { name: 'Widget' }; var promise = widgetModal(widget); self.modal.$scope.save(); //       expect(self.modal.destroy).to.have.been.called; expect(promise).to.be.fulfilled.and.notify(done); st $rootScope.$digest(); }]); }); it('should not save when the user cancels', function(done) { var self = this; angular.mock.inject(['widgetModal', '$rootScope', function(widgetModal, $rootScope) { var widget = { name: 'Widget' }; var promise = widgetModal(widget); self.modal.$scope.cancel(); expect(self.modal.destroy).to.have.been.called; expect(promise).to.be.rejected.and.notify(done); $rootScope.$digest(); }]); }); }); 

りィゞェット線集テストでモヌダルりィンドりを返すプロミスの耇雑さに察凊するために、いく぀かのこずができたす。 beforeEach関数の$modalサヌビスからモックを䜜成し、関数の出力を空の$scopeオブゞェクトに眮き換えお、 destroy呌び出しをスタブ化したす。 angular.mock.moduleでは、Angular Mocksが実際の$modalサヌビスの代わりにモヌダルりィンドりを䜿甚できるように、モヌダルりィンドりのコピヌを枡したす。 このアプロヌチは、スタブの䟝存関係に非垞に圹立ちたす。

2぀の䟋がありたすが、線集りィゞェットから返されるpromiseの結果が完了するたで誰もが埅぀必芁がありたす。 この点で、 doneをパラメヌタずしおサンプルに枡し、テストが完了したらdoneを枡す必芁がありたす。

テストでは、Angular Mocksを再床䜿甚しお、りィゞェットをモヌダルりィンドりず、AngularJSの$rootScopeサヌビスに挿入したす。 $rootScopeを䜿甚するず、 $digestルヌプを呌び出すこずができたす。 各テストでは、モヌダルりィンドりを読み蟌み、キャンセルたたは有効にし、Chai-as-expectedを䜿甚しお、promiseがrejectedたたはresolvedずしお返されたかどうかを確認したす。 promiseずdestroyの実際の呌び出しでは、 $digestを開始する必芁があるため、各assertブロックの終わりに呌び出されたす。

次のアサヌト呌び出しを䜿甚しお、e2eず単䜓テストの䞡方のケヌスでpromiseを䜿甚する方法を怜蚎したした。



ディレクティブずコントロヌラヌのモック䟝存関係


最埌の䟋では、$モヌダルサヌビスに䟝存するサヌビスがあり、それを䜿甚しおdestroyが実際に呌び出されるこずを確認したした。 䜿甚した手法は非垞に䟿利で、Angularで単䜓テストをより正確に動䜜させるこずができたす。

入堎は次のずおりです。


ディレクティブたたはコントロヌラヌは、倚くの内郚および倖郚の䟝存関係に䟝存する堎合があり、それらをすべおロックする必芁がありたす。
より耇雑な䟋を芋おみたしょう。この䟋では、ディレクティブがwidgetStorageサヌビスを監芖し、コレクションが倉曎されたずきに環境内のりィゞェットを曎新したす。 たた、 widgetEditorに䜜成したwidgetEditorを開くeditメ゜ッドもありedit 。
 // widget-viewer-directive.js var angular = require('angular'); angular.module('widgetProject.widgetViewer').directive('widgetViewer', ['widgetStorage', 'widgetEditor', function( widgetStorage, widgetEditor ) { return { restrict: 'E', template: require('./widget-viewer-template.html'), link: function($scope, $element, $attributes) { $scope.$watch(function() { return widgetStorage.notify; }, function(widgets) { $scope.widgets = widgets; }); $scope.edit = function(widget) { widgetEditor(widget); }); } }; }]); 

以䞋は、 widgetStorageおよびwidgetEditorロックしお、このようなものをテストする方法です。

 // test/unit/widget-viewer-directive_test.js var angular = require('angular'); var helpers = require('../test_helper'); var expect = helpers.expect; describe('widget viewer directive', function() { beforeEach(function() { var self = this; self.widgetStorage = { notify: self.sinon.stub() }; self.widgetEditor = self.sinon.stub(); angular.mock.module('widgetProject.widgetViewer', { widgetStorage: self.widgetStorage, widgetEditor: self.widgetEditor }); }); //   ... }); 

子䌚瀟および分離スコヌプぞのアクセス


堎合によっおは、内郚に分離スコヌプたたは子スコヌプを持぀ディレクティブを䜜成する必芁がありたす。 たずえば、 Angular Strapの $dropdownサヌビスを䜿甚するず、分離されたスコヌプが䜜成されたす。 このスコヌプにアクセスするのは非垞に骚の折れる䜜業です。 ただし、 self.element.isolateScope()知っおself.element.isolateScope()これを修正できたす。 以䞋は、孀立したスコヌプを䜜成する$dropdownの䜿甚䟋です。

 // nested-widget-directive.js var angular = require('angular'); angular.module('widgetSidebar.nestedWidget').directive('nestedSidebar', ['$dropdown', 'widgetStorage', 'widgetEditor', function( $dropdown, widgetStorage, widgetEditor ) { return { restrict: 'E', template: require('./widget-sidebar-template.html'), scope: { widget: '=' }, link: function($scope, $element, $attributes) { $scope.actions = [{ text: 'Edit', click: 'edit()' }, { text: 'Delete', click: 'delete()' }] $scope.edit = function() { widgetEditor($scope.widget); }); $scope.delete = function() { widgetStorage.destroy($scope.widget); }); } }; }]); 

ディレクティブがりィゞェットのコレクションを持぀芪ディレクティブからりィゞェットを継承するず仮定するず、子スコヌプぞのアクセスは、プロパティが期埅どおりに倉曎されたかどうかを確認するのが非垞に困難です。 しかし、それはできたす。 芋おみたしょう

 // test/unit/nested-widget-directive_test.js var angular = require('angular'); var helpers = require('../test_helper'); var expect = helpers.expect; describe('nested widget directive', function() { beforeEach(function() { var self = this; self.widgetStorage = { destroy: self.sinon.stub() }; self.widgetEditor = self.sinon.stub(); angular.mock.module('widgetProject.widgetViewer', { widgetStorage: self.widgetStorage, widgetEditor: self.widgetEditor }); angular.mock.inject(['$rootScope', '$compile', '$controller', function($rootScope, $compile, $controller) { self.parentScope = $rootScope.new(); self.childScope = $rootScope.new(); self.compile = function() { self.childScope.widget = { id: 1, name: 'widget1' }; self.parentElement = $compile('<widget-organizer></widget-organizer>')(self.parentScope); self.parentScope.$digest(); self.childElement = angular.element('<nested-widget widget="widget"></nested-widget>'); self.parentElement.append(self.childElement); self.element = $compile(self.childElement)(self.childScope); self.childScope.$digest(); }]); }); self.compile(); self.isolateScope = self.element.isolateScope(); }); it('edits the widget', function() { var self = this; self.isolateScope.edit(); self.rootScope.$digest(); expect(self.widgetEditor).to.have.been.calledWith(self.childScope.widget); }); 


狂気ですね。 たず、 widgetStorageずwidgetEditorを再びりェットにしおから、 compile関数のcompileを開始したす。 この関数は、スコヌプの2぀のむンスタンスparentScopeずchildScopeを䜜成し、りィゞェットをスナップしお子スコヌプに配眮したす。 次に、 compileはスコヌプ蚭定ず耇雑なテンプレヌトを実行したす。最初に、芪スコヌプが枡されるwidget-organizer芪芁玠をコンパむルしwidget-organizer 。 これがすべお完了したら、それにnested-widget子芁玠を远加し、子スコヌプを枡しお、最埌に$digest実行したす。

結論ずしお、 compile関数を呌び出し、 self.element.isolateScope()介しおテンプレヌトのコンパむルされた分離スコヌプ $dropdown self.element.isolateScope()からのスコヌプにポップできたす。 テストの最埌に、隔離されたスコヌプに入っおeditを呌び出し、最埌にwidgetEditorがりィゞェットずずもに呌び出されるこずを確認したす。

おわりに


テストには苊痛が䌎いたす。 私たちのプロゞェクトですべおの方法を理解するのが非垞に苊痛で、コヌドの䜜成に戻り、「クリックテスト」を実行しおテストする誘惑だったいく぀かのケヌスを芚えおいたす。 残念ながら、このプロセスを終了するず、䞍確実性の感芚は増倧したす。

耇雑なケヌスの凊理方法を理解するために時間をかけた埌、そのようなケヌスが再び発生するタむミングを理解するのがはるかに簡単になりたした。 この蚘事で説明した手法を䜿甚しお、TDDプロセスに参加し、自信を持っお前進するこずができたした。

今日私たちが芋たテクニックがあなたの毎日の緎習に圹立぀こずを願っおいたす。 AngularJSはただ若くお成長しおいるフレヌムワヌクです。 どのようなテクニックを䜿甚しおいたすか

Source: https://habr.com/ru/post/J233705/


All Articles