はじめに
テストコードを書けない、書こうとしない毎日をおくっていましたが、
いよいよ逃げ切れなくなってきたので、頑張ってやってみたいと思います。
間違っていたらコメントいただければありがたいです。
ユニットテストが必要なケース
いきなりなのですが、私だけかもしれませんが、ユニットテストの入れどころがよくわからなかったです。
他の方の引用になるが、下記などがユニットテストしたほうがよい項目の参考になるかもしれません。
・複数箇所で共有する汎用のJSモジュールを作成した場合
・AJAXを利用したAPIとの通信が発生する場合
・データを処理するビジネスロジックが発生した場合
・関数へ渡す引数パターンが複数存在する場合
##前提
KarmaとJasmineを使う。
angularjsも使う。
Karma
テストフレームワーク。
テスト環境を提供するツールです。
Jasmine
テストランナー。
テストを実行する為のツールです。
##ユニットテストの準備
###Karmaをインストール
Karma本体と、Karmaコマンドを利用する為のクライアントツールのkarma-cliをインストール
npm install karma
sudo npm install -g karma-cli
###Jasmineをインストール
npm install --save-dev karma-jasmine
###Karmaを初期化する
下記コマンドを実行して、いくつかの質問に答えるとkarma.conf.jsが作成される。
karma init
# 利用するテストフレームワーク
Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine
# Require.js を利用するか
Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no
# テストランナーが利用するブラウザを選択(複数指定が可能)
Do you want to capture any browsers automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> PhantomJS
>
# テスト対象とテストコードとなるファイル。空欄で後から指定しても問題ない
What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
>
# テスト対象ファイルの中で、除外したいファイル(対象ファイルで、**/**.jsとかした場合に使う)
Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
>
# コード変更時にテストを再実行するかどうか
Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes
ユニットテストをするには
とりあえず最低限下記の4つが必要だと思う
- describeでテストグループを作成し
- itでテストケースを作成し
- expectでテスト対象を渡し
- Matcher(toEqualとか)でテスト対象をが期待した値かをチェック!
- 下記のコマンドで1~4を実行
karma start
describe
describeでテストグループ名を定義する。
describe('テストのグループ', function() {
//テスト内容を記載
});
下記の用に入れ子にすることもできる
describe('テストのグループ', function() {
describe(テスト名,function(){
});
});
it
itでテストケースを定義する。
describe('テストのグループ', function() {
//テスト内容を記載
it('テストケース',function(){
});
});
expectとMatcher
テスト対象が、期待する値かを確認する為の関数。
expect
でテスト対象を渡し、Matcher
で渡された対象が、期待する結果かどうかをチェックします。
describe('テストのグループ', function() {
//テスト内容を記載
it('テストケース',function(){
expect(1).toEqual(1);//テストは成功する
});
});
上記でテストを実行すると、1と1を比較して同じ値なので結果はSUCCESSとなる。
PhantomJS 1.9.8 (Mac OS X 0.0.0): Executed 1 of 1 SUCCESS (0.007 secs / 0.033 secs)
describe('テストのグループ', function() {
//テスト内容を記載
it('テストケース',function(){
expect(1).toEqual(2);//テストは失敗する
});
});
上記でテストを実行すると、1と2を比較して異なる値なので結果はFAILEDとなる。
PhantomJS 1.9.8 (Mac OS X 0.0.0) テストのグループ ログイン処理 FAILED
jasminから提供されているMatcher
Matcherは自分で独自に定義もできるようですが、
下記のものが用意されています。
下記参考
関数名 | 説明 |
---|---|
expect(a).toEqual(b) | aがbと同値であることを期待する |
expect(a).toBe(b) | aがbと同一オブジェクトであることを期待する |
expext(a).toBeDefined() | aが定義されていることを期待する(undefinedでない) |
expect(a).toBeNull() | aがnullであることを期待する |
expect(a).toBeTruthy() | aがtrueであることを期待する |
expect(a).toBeFalsy() | aがfalseであることを期待する |
expect(a).toContain(b) | aにbが含まれていることを期待する |
expect(a).toBeLessThan(b) | aがbより小さいことを期待する |
expect(a).toBeGreaterThan(b) | aがbより大きいことを期待する |
expect(fn).toThrow(e) | fnが例外をスローすることを期待する |
否定形をテストをする場合は、notでつなぐ。
expect(a).not.toEqual(b);//aがbと異なる値であることを期待する
beforeEach
テスト単位で、テスト前に必ず実行される処理。
module
関数はangularモジュールを読み込むために使用します。
describe('テストのグループ', function() {
beforeEach(module('app'));
});
inject
は必要なサービスのインジェクトする為に使用します。
describe('テストのグループ', function() {
beforeEach(module('app'));
it('テストケース',inject(function(getMyNameService){
expect(getMyNameService()).toEqual('太郎');
}));
});
module関数と、inject関数はangular-mock.jsで提供されている関数なので、angularjsを使用するには必ずテスト対象ファイルとして読み込む必要があります
afterEach
テスト単位で、テスト後に必ず実行される処理。
andularjsのモジュール、サービスを使ってテストする
上記でも少しかきましたが、angularjsを使っているコードをテストする場合、
必ずやらなければならないことが幾つかあります。
使うモジュールは明示的に読み込む必要があります。
describe('テストのグループ', function() {
beforeEach(module('app'));
});
利用するサービスを読み込む必要があります。
describe('テストのグループ', function() {
var $filter;
beforeEach(module('app'));
beforeEach(inject(function(_$filter_){
$filter = _$filter_;
}));
});
ちょっとテストの話からそれますが、
サービスをインジェクトする際の、_$filter_
アンダースコアが何なのかがわかりませんでした。
$filter
でも動くので作法てき何かかと思っていたのですが、どうも違うみたいです。
テストコードを読みやすくする為に、自分で定義した変数名を、サービス名と同じにしたい。
でも、依存注入時の変数と衝突してしまう。
それを回避する為に、angularjsではアンダースコアラッピングという方法が用意されているようです。
下記に詳しく載ってます。
フィルターをテストしてみる
filterサービスをテストしてみます。
全角英数字を半角英数字に変換するfilterです。
angular.module('app',[])
.filter('oneByte', [function() {
return function(input) {
return input.replace(/[A-Za-z0-9]/g, function(s) {
return String.fromCharCode(s.charCodeAt(0) - 0xFEE0);
});
}]);
describe('テストのグループ', function() {
var $filter;
beforeEach(module('app'));
beforeEach(inject(function(_$filter_){
$filter = _$filter_;
}));
it('全角英数字を半角に変換するテスト', function() {
var oneByte = $filter('oneByte');//使いたいfilterをセット
expect(oneByte('12A')).toEqual('12A');
});
});
PhantomJS 1.9.8 (Mac OS X 0.0.0): Executed 8 of 8 SUCCESS (0.006 secs / 0.062 secs)
controllerをテストしてみる
angular.module('app',[])
.controller('MyController', ['$scope',function($scope) {
$scope.name = '太郎';
$scope.setName = function(str) {
$scope.name = str;
}
}]);
describe('テストのグループ', function() {
var scope;
var $controller;
var $rootScope;
beforeEach(module('app'));
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$controller('MyController', {
$scope: scope
});
}));
it('名前を設定するテスト', function() {
expect(scope.name).toEqual('太郎');
scope.setName('花子');
expect(scope.name).toEqual('花子');
});
});
controllerをテストするには、まずcontrollerサービスをインジェクトします。
そして、controllerサービスを使ってコントローラーをインスタンス化します。
インスタンス化する方法は、下記の方法になります。
$controller(コントローラー名,{引数名:値})
MyControllerは、引数としてscopeオブジェクトを受け取っているので、
明示的にscopeを$controllerサービスに渡します。
beforeEach(inject(function(_$controller_, _$rootScope_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
$controller('MyController', {
$scope: scope
});
}));
$controllerサービスは戻り値としてコントローラーのインスタンスを返します。
var instance = $controller('MyController', {
$scope: scope
});
scopeオブジェクトを作成するには、下記の方法になります。
$rootScope().$new(isolate);
isolate ・・・親スコープを継承するかしないかのフラグ。デフォルトはfales。
isolateをtrueにした場合は、親スコープからプロトタイプ継承を行わず、独立した分離スコープを作成します。
directiveをテストしてみる
directiveをテストする際は、下記が必要なようです。
・directiveのエレメントを作成する
・directiveのエレメントをcompileする。
・directiveのエレメントとscopeをリンクさせる。
・digestループを発生させる。
コンパイル処理
コンパイル処理は$compileサービスを利用する。
※$compoileは、通常はangularjsがディレクティブを処理するために自動でおこなっています。
テスト時は手動で行う必要があります。
var link = $compoile(element,transclude,priority);
element・・・コンパイル対象要素(文字列、もしくはjqLietオブジェクトを指定できる)
transclude・・・ディレクティブで利用する関数(非推奨)
priority・・・優先順位
appModule.directive('testDirective', [function() {
return {
'restrict': 'E',
'link': function(scope) {
scope.name = '太郎';
scope.setName = function(str) {
scope.name = str;
};
}
};
}]);
describe('テストのグループ', function() {
var scope;
var $rootScope;
var $compile;
beforeEach(module('app'));
beforeEach(function() {
spyOn(ons, 'isWebView').and.returnValue(true);
});
beforeEach(inject(function( _$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
scope = $rootScope.$new();
}));
it('ディレクティブのテスト', function() {
var element = angular.element('<test-directive>{{name}}</test-directive>');
var link = $compile(element);
link(scope);
scope.$digest();
expect(element.text()).toEqual('太郎');
scope.setName('花子');
scope.$digest();
expect(element.text()).toEqual('花子');
});
});
##HTTP通信のテストをしてみる
HTTP通信のテストを実行する場合、モックを利用する。
モックとは
ユニットテストの為のダミーオブジェクト。
モックを利用することで実際はサーバーに接続が必要な通信処理を、
サーバーに接続したことにして、仮の値を返すことができます。
angularjsのユニットテストでモックを利用する場合は、angular-mock.jsを読み込む必要がある。
###$httpBackendを使用して通信処理のテストを試す
angular.module('app',[])
.controller('AccountCtrl', function(){
$scope.getName = function() {
$http.get('/name')
.success(function(data) {
$scope.name = data;
})
.error(function(){
$scope.phones = '';
});
}
});
describe('テストグループ', function() {
var $controller;
var $httpBackend;
var $rootScope;
var $scope;
var response = [{name: '太郎'}, {name: '花子'}];
beforeEach(module('app'));
beforeEach(inject(function(_$controller_, _$rootScope_, _$httpBackend_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$httpBackend = _$httpBackend_;
$scope = scope = $rootScope.$new();
var controller = $controller('myCtrl', {
$scope: $scope
});
}));
it('HTTP通信のテスト', function() {
expect($scope.phones).toBeUndefined();
$scope.getName();
$httpBackend.expect('GET','/name').respond(response);
$httpBackend.flush();
expect(scope.name).toEqual([{
name: '太郎'
}, {
name: '花子'
}]);
$scope.getName();
$httpBackend.expect('GET','/name').respond(500,response);
$httpBackend.flush();
expect(scope.name)toEqual('');
});
});
$httpBackendを利用する場合は、下記がポイントとなります。
・expectで想定されるリクエストを定義
・respondで対応するレスポンスを定義
・flushメソッドでリクエストをフラッシュする
expect
expect(method,url,data,headers)
method ・・・HTTPメソッド(GET,POST,PUT,DELETEなど)
url ・・・リクエストurl
data ・・・リクエストボディ
headers・・・リクエストヘッダー
respond
respond(status,data,headers)
status・・・応答ステータスコード(200など)
data ・・・レスポンスボディ
header・・・レスポンスヘッダー({ヘッダー名:値})
flush
フラッシュするという表現がよくわからなかった。
単語の意味はトイレに水を流すとかの意味になるようです。
つまり、ウンコ(リクエスト)をして、水を流す(実行する)ようなイメージだろうか。
flush(count)
count・・・
フラッシュ(到着した順に)するレスポンスの数値を指定します。
もし未定義であれば、保留中の全てのリクエストがフラッシュされます。
もし、flushメソッドが呼ばれた際に、保留中のリクエストが無ければ、スローされます。
###終わり
まだ2割くらいしか理解していませんが、今後も学んだことを追記していこうと思います。