開発者は皆、開発プロジェクトにおいて単体テストが非常に有益であることに同意しています。これらはコードの品質を確保するのに役立ち、それによってリファクタリングが必要な場合でも開発の安定性と信頼性が高まります。
テスト駆動開発フローチャート
AngularJS のコードのテスト容易性が高いという主張は確かに合理的です。このドキュメントにリストされているエンドツーエンドのテスト例だけでも、これを説明できます。 AngularJS のようなプロジェクトでは、単体テストは簡単だと言われていますが、それをうまく行うのは簡単ではありません。公式ドキュメントには詳細な例が記載されていますが、実際のアプリケーションでは依然として非常に困難です。ここでは簡単に操作方法を説明します。
インスタントカルマ
Karma は、JavaScript 用に Angular チームによって開発されたテスト実行フレームワークです。テスト タスクを簡単に自動化し、面倒な手動操作 (回帰テスト セットやターゲット テストの依存関係の読み込みなど) を置き換えます。Karma と Angular のコラボレーションは、ピーナッツ バターとゼリーのようなものです。
Karma で構成ファイルを定義して起動するだけで、予期されるテスト環境でテスト ケースが自動的に実行されます。関連するテスト環境を構成ファイルで指定できます。 angular-seed は、私が強くお勧めするソリューションであり、すぐに実装できます。私の最近のプロジェクトにおける Karma の構成は次のとおりです:
module.exports = function(config) { config.set({ basePath: '../', files: [ 'app/lib/angular/angular.js', 'app/lib/angular/angular-*.js', 'app/js/**/*.js', 'test/lib/recaptcha/recaptcha_ajax.js', 'test/lib/angular/angular-mocks.js', 'test/unit/**/*.js' ], exclude: [ 'app/lib/angular/angular-loader.js', 'app/lib/angular/*.min.js', 'app/lib/angular/angular-scenario.js' ], autoWatch: true, frameworks: ['jasmine'], browsers: ['PhantomJS'], plugins: [ 'karma-junit-reporter', 'karma-chrome-launcher', 'karma-firefox-launcher', 'karma-jasmine', 'karma-phantomjs-launcher' ], junitReporter: { outputFile: 'test_out/unit.xml', suite: 'unit' } }) }
これは angular-seed のデフォルト設定に似ていますが、次の違いがあります:
autoWatch は非常に優れた設定で、ファイルが変更されたときに Karma が自動的にテスト ケースに戻るようにします。 Karma は次のようにインストールできます:
npm install karma
angular-seed は、Karma テストをトリガーするための簡単なスクリプト inscripts/test.sh を提供します。
Jasmine を使用してテスト ケースを設計する
ほとんどのリソースは、動作駆動型開発モデルを備えた JavaScript テスト フレームワークである Jasmine を使用して Angular の単体テスト ケースを設計するときにすでに利用可能です。
これが次に話したいことです。
AngularJS コントローラーの単体テストを行う場合は、Angular の依存関係注入を使用できます。 依存関係注入 この関数は、テスト シナリオでコントローラーに必要なサービス バージョンをインポートし、期待される結果が正しいかどうかもチェックします。たとえば、次のコントローラを定義して、移動する必要があるタブを強調表示します:
app.controller('NavCtrl', function($scope, $location) { $scope.isActive = function(route) { return route === $location.path(); }; })
isActive メソッドをテストしたい場合はどうすればよいですか? $locationservice 変数が期待値を返し、メソッドが期待値を返すかどうかを確認します。したがって、テストの説明では、テスト中に必要な制御されたバージョンを保存し、必要に応じて対応するコントローラーにそれを挿入するローカル変数を定義します。次に、実際のテストケースにアサーションを追加して、実際の結果が正しいかどうかを検証します。全体のプロセスは次のとおりです:
describe('NavCtrl', function() { var $scope, $location, $rootScope, createController; beforeEach(inject(function($injector) { $location = $injector.get('$location'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('NavCtrl', { '$scope': $scope }); }; })); it('should have a method to check if the path is active', function() { var controller = createController(); $location.path('/about'); expect($location.path()).toBe('/about'); expect($scope.isActive('/about')).toBe(true); expect($scope.isActive('/contact')).toBe(false); }); });
基本構造全体を使用して、さまざまなタイプのテストを設計できます。このテスト シナリオではローカル環境を使用してコントローラーを呼び出すため、さらに属性を追加し、これらの属性をクリアするメソッドを実行して、属性がクリアされたかどうかを確認することもできます。
$httpBackend はクールです
それでは、$httpservice を呼び出してサーバーにデータをリクエストまたは送信する場合はどうなるでしょうか?幸いなことに、Angular は
を提供します。$httpBackend のモックメソッド。このようにして、サーバーの応答コンテンツをカスタマイズしたり、サーバーの応答結果が単体テストでの期待と一致していることを確認したりできます。
具体的な詳細は次のとおりです:
describe('MainCtrl', function() { var $scope, $rootScope, $httpBackend, $timeout, createController; beforeEach(inject(function($injector) { $timeout = $injector.get('$timeout'); $httpBackend = $injector.get('$httpBackend'); $rootScope = $injector.get('$rootScope'); $scope = $rootScope.$new(); var $controller = $injector.get('$controller'); createController = function() { return $controller('MainCtrl', { '$scope': $scope }); }; })); afterEach(function() { $httpBackend.verifyNoOutstandingExpectation(); $httpBackend.verifyNoOutstandingRequest(); }); it('should run the Test to get the link data from the go backend', function() { var controller = createController(); $scope.urlToScrape = 'success.com'; $httpBackend.expect('GET', '/slurp?urlToScrape=http:%2F%2Fsuccess.com') .respond({ "success": true, "links": ["http://www.google.com", "http://angularjs.org", "http://amazon.com"] }); // have to use $apply to trigger the $digest which will // take care of the HTTP request $scope.$apply(function() { $scope.runTest(); }); expect($scope.parseOriginalUrlStatus).toEqual('calling'); $httpBackend.flush(); expect($scope.retrievedUrls).toEqual(["http://www.google.com", "http://angularjs.org", "http://amazon.com"]); expect($scope.parseOriginalUrlStatus).toEqual('waiting'); expect($scope.doneScrapingOriginalUrl).toEqual(true); }); });
ご覧のとおり、beforeEach の呼び出しは実際には非常に似ています。唯一の違いは、$httpBackend を直接取得するのではなく、インジェクターから取得することです。それでも、さまざまなテストを作成すると、明らかな違いがいくつかあります。まず、各ユースケースの実行後に $httpBackend に明らかな異常なリクエストがないことを保証する afterEachcall メソッドがあります。テスト シナリオの設定と $httpBackend メソッドのアプリケーションを見ると、あまり直感的ではない点がいくつかあることがわかります。
実際、$httpBackend を呼び出す方法はシンプルで明確ですが、それだけでは十分ではありません。実際のテストでは、$scope に値を渡すメソッドで呼び出しを $scope.runTest メソッドにカプセル化する必要があります。 .$適用します。こうすることで、$digest がトリガーされた後にのみ HTTP リクエストを処理できます。ご覧のとおり、$httpBackend は、$httpBackend.flush() メソッドを呼び出すまで解析されません。これにより、呼び出し中に返された結果が正しいかどうかを確認できます (上記の例では、コントローラーの $scope.flush)。 parseOriginalUrlStatusproperty プロパティは呼び出し元に渡されるため、リアルタイムで監視できます)
次の数行のコードは、呼び出し中に $scopethat 属性を検出するアサーションです。クールですよね?
ヒント: 一部の単体テストでは、ユーザーは変数として $ を付けずにスコープをマークすることに慣れています。これは Angular ドキュメントでは必須でも強調されているわけでもありませんが、読みやすさと一貫性を向上させるために $scopelike を使用しています。
結論
これは私にとって自然なことの 1 つかもしれませんが、Angular で単体テストの書き方を学ぶことは、最初は私にとって間違いなく非常に苦痛でした。開始方法に関する私の理解の多くは、インターネット上のさまざまなブログ投稿やリソースのつぎはぎから得たものであり、実際に一貫した明確なベスト プラクティスはなく、むしろ自然に思いついたランダムな選択によって得られたものであることがわかりました。 Angular と Jasmine の使用方法のすべての奇妙でユニークな機能を理解する必要はなく、ただコードを書きたいだけで苦労している人を助けるために、私が最終的に完成したものについてのドキュメントを提供したいと思いました。そこで、この記事が少しでもお役に立てれば幸いです。