お久しぶりでございます。scalaでバックエンドを開発しているaxtstar(@axtstart)でございます。
前回はマクロって言ったり、関数って言ったり、Functionだったりする何か。~Google Spread Sheet~
の話をしましたが、今回は、Typescriptを使って、Javascriptで書かれたテストコードを効率化した話を書いてみようと思います。
TL;DR
Typescriptとデザインパターンの「テンプレート・メソッド・パターン」が分かる方はこの記事は読まずに、
「エンジニア・デザイナーの採用情報」のバナーからぜひ弊社にご応募ください。
テストは重要
プロダクションをデプロイする前に、テストを行うのが当然なのはご存知の通りですが、
私の担当してるプロジェクトでは、導通確認やトランザクションチェックにヘッドレスブラウザ*1を用いた試験を導入しています。
デプロイした後のプロダクションに対してもテストを実行しシステムの健全動作を定期的に確認しています。
↓こんなテスト↓
STEP1 → STEP2 → STEP3 → STEP4
STEPは各ページに相当
→は画面遷移
PhantomJS
担当プロジェクトでは経緯があって、PhantomJSを上記テストに利用しています。
PhantomJSは、
- Javascript(ES5)互換であること
- ヘッドレスブラウザ(Qt web kitを使用)であること
- 画面キャプチャ機能があること
- NodeJS上で動くわけではないこと*2
- 残念ながら主要メンテナーの方が最近ステップダウンされた*3
などの特徴があり、よく知られたヘッドレステストツールです。
問題点
しかしながら、Javascriptで記述していたためというわけでは無いのですが、
以下の問題を抱えている状態でした。
- 可読性が低い(あくまで処理の順番通りにコードが書かれているだけ)
- 構造化されていない(処理が最初から終わりまで1メソッドで書かれていたりする)
- 似て非なるテストが多くあり、メンテナンスに時間を要する
- あるテストではAssertがあるのに他のものではなかったりする(テスト品質にバラツキがある)
這い寄る混沌
そんな中、テスト全てに影響のある修正が行われることになりました。
修正そのものは(それほど)難しくは無いのですが、それが動作する動線が多く、このためのテスト対応を、
現状のソースに対して行うのはSAN値が下がる大変だと直感しました。
↓ちなみにこのような修正↓
STEP1 → STEP2 → STEP3 → STEP4
↓
STEP1 → STEP1-2 → STEP2 → STEP3 → STEP4
そこで今回このコードを一気にモダン化しようと思い至りました。
AltJS導入
かつては、CoffeeScriptのソースが直接読み込めたPhantomJSですが、version 2以降はメンテナンスの関係からその対応は外されています。*4
ですので、モダン化を行うには、現状、AltJSと呼ばれる、対象の言語を、PhantomJSが動作するJavascriptにトランスパイルする言語・環境が必要になってきます。
こちらのエントリでBabel、ES6の話を書いているので初めはBabelによるES5へのトランスパイルを試そうかな*5と思っていました。
しかし、@typesの記事を目にして、Typecriptを採用してみることにしました。
※これ以前はtsdとかTypingsとかNodeJSの設定以外にも色々手を加える部分が必要なTypescriptはとっつきにくいなと感じていました。
修正前
さて、
「そのもの」のソースはお見せできないので、今回、大幅に簡略、概念化した、修正前のソースを用意しました。
Webページの遷移において、ユーザが好きな何かを選択して画面遷移するもの、としています。
※そうは言っても、あくまで今回のブログ用に作成したものなので、「そのもの」よりかなり可読性は高いです。
phantom.casperPath = './node_modules/casperjs';
phantom.injectJs('./node_modules/casperjs/bin/bootstrap.js');
phantom.casperTest = true;
var casper = require('casper').create();
var fs = require('fs');
var url = "http://sample.com/";
var wait_time= 300;
var title;
casper.test.begin('test start', function suite(test) {
casper.userAgent('Mozilla/5.0 (Macintosh略');
casper.start().viewport(1024, 768).thenOpen(url, function() {
test.assertTitle("タイトル", "title is タイトル");
title = this.getTitle();
});
casper.thenEvaluate(function() {
document.querySelector('input[name="ログインID"]').value = 'ログインID';
document.querySelector('input[name="パスワード"]').value = 'パスワード';
document.querySelector('#clickable').click();
}, '')
.then(function() {
this.test.assertTitleMatch(/STEP1/, "title contains STEP1");
this.wait(wait_time, function() {
this.fillSelectors('form', {
'select[name="好きな映画"]': 'マッドマックス 怒りのデス・ロード',
'select[name="好きな車"]': 'Peugeot 106'
}, false);
this.click('#clickable');
});
})
.then(function() {
this.wait(wait_time, function() {
this.test.assertTitleMatch(/STEP2/, "title contains STEP2");
this.fillSelectors('form', {
'select[name="好きなチーム"]': '阪神タイガース',
'select[name="好きな選手"]': 'ジネディーヌ・ジダン'
}, false);
this.click('#clickable');
});
})
.then(function() {
this.wait(wait_time, function() {
this.test.assertTitleMatch(/STEP3/, "title contains STEP3");
this.fillSelectors('form', {
'select[name="好きな国"]': 'フランス',
'select[name="好きな曲"]': '愛なんだ'
}, false);
this.click('#clickable');
});
})
.then(function() {
this.wait(wait_time, function() {
this.test.assertTitleMatch(/STEP4/, "title contains STEP4");
this.fillSelectors('form', {
'select[name="好きな番組"]': '正解するカド',
'select[name="好きな本"]': 'The Three Body Problem'
}, false);
this.click('#clickable');
});
});
casper.run(function () {
test.done();
})
.then(function(){
this.exit();
});
});
このようなJSファイルが数十個あるような状態でした。
このソース群一個一個個別に、同じ追加の処理を書いて行くのは、やはり遠い目になるのは間違いないので、
今見てもこの選択は良かったなと思っています。
目標
Typescript導入による目標を下記のごとく定めました。
- 構造化による差分プログラムを可能にすること
- Enum化によるインテリセンスの活用、値入力の効率化、ランダム取得対応
構造化
上記を基本として、必要に応じてクラスをオーバライドすることで、
各処理に対応するようにしました。
また管理したい単位でクラス設計を行いました。
export class People{
public loginID:string
public passwd:string
public movie:string = MovieEnum.fury
public car:string = CarEnum.peugeot106
}
入力のものはstring、選択式のものはenumを使用しました(後述)
↓構造↓
基本構造として、URLを開いてログインするだけで終わる処理をBASEとして記述します。
そのフローの中で、何もしない処理を記述します
Start → Login → Step1(何もしない) → Step2(何もしない) → Step3(何もしない) → Step4(何もしない)
export module TestModule {
export class Register {
public url = "http://sample.com"
public people:People = new OrdinalPeople()
protected beginningString:string = "start"
protected assertTitle:string
protected sizeWidth:number = 1024
protected sizeHeight = 768
protected emitStart = () => {
var url = `${this.url}`
var assertTitle = this.assertTitle
var sizeWidth = this.sizeWidth
var sizeHeight = this.sizeHeight
casper.start().viewport(sizeWidth, sizeHeight).thenOpen(url, function() {
var title = this.getTitle()
casper.test.assertTitle(assertTitle, "title is " + title)
})
}
protected emitLogin = () => {
var loginID = this.people.loginID
var passwd = this.people.passwd
casper.thenEvaluate(function(loginID:any,passwd:any) {
(<HTMLInputElement>document.querySelector('input[name="ログインID"]')).value = loginID
(<HTMLInputElement>document.querySelector('input[name="パスワード"]')).value = passwd
(<HTMLButtonElement> document.querySelector("#clickable")).click()
}, loginID, passwd)
}
protected emitStep1 = () => {}
protected emitStep2 = () => {}
protected emitStep3 = () => {}
protected emitStep4 = () => {}
protected emitRun = () => {
casper.run(function () {
casper.test.done()
})
.then(function(){
this.exit()
})
}
protected casperFunc = () => {
var emitStart = this.emitStart
var emitLogin = this.emitLogin
var emitStep1 = this.emitStep1
var emitStep2 = this.emitStep2
var emitStep3 = this.emitStep3
var emitStep4 = this.emitStep4
var emitRun = this.emitRun
casper.test.begin(this.beginningString, function suite(test:any) {
emitStart();
emitLogin();
emitStep1();
emitStep2();
emitStep3();
emitStep4();
emitRun();
});
}
}
}
上記のクラスを継承し、Step1の処理を差分で書き換えるようにしました
Start → Login → Step1(映画、車) → Step2(何もしない) → Step3(何もしない) → Step4(何もしない)
export class MovieRegister extends Register {
people = new OrdinalPeople()
getStep1 = (people:People) =>{
return {
'input[name="好きな映画"]':people.movie,
'input[name="好きな車"]': people.car
}
}
emitStep1 = () => {
var step1 = this.getStep1(this.people)
casper.then(function() {
this.fillSelectors('form', step1, false)
this.click('#clickable');
})
}
}
Enum化
設定値をEnum化することで、テストコーディング時にインテリセンス(タブ補完、オートコンプリーション)による、効率的なコーディングを可能にしました。
export enum UserAgentEnum {
PC = <any>'Mozilla/5.0 略',
SP = <any>"Mozilla /5.0 (iPhone;略"
}
Stringのenum型が許容されないため、文字列の前に<any>を指定する必要がありました。
トランスパイルされたJavascriptは下記のようになっていました。
var UserAgentEnum;
(function (UserAgentEnum) {
UserAgentEnum[UserAgentEnum["PC"] = 'Mozilla/5.0 略'] = "PC";
UserAgentEnum[UserAgentEnum["SP"] = "Mozilla /5.0 (iPhone;略"] = "SP";
})(UserAgentEnum = TenCasper.UserAgentEnum || (TenCasper.UserAgentEnum = {}));
このため、例えばランダムで値を取得したい(ランダムでUserAgentを変更したい)場合サイズ取得時に2で割る必要がありました。
export function getRandomEnum(o:any) {
var len = Object.keys(o).length / 2
var x = Math.floor(Math.random() * len)
return <any>Object.keys(o)[x]
}
このような形で選択式の部分をEnum化しTypescriptの特徴である、型システムを最大限利用できるようにしました。
また、Enum値のランダム取得を行うことで、網羅率を高める取り組みが比較的容易になったかなと思います。
朗報
Typescript2.4 から stringのenumがサポートされるようになりました。*6
export enum UserAgentEnum {
PC = 'Mozilla/5.0 略',
SP = "Mozilla /5.0 (iPhone;略"
}
↓トランスパイル↓
var UserAgentEnum;
(function (UserAgentEnum) {
UserAgentEnum["PC"] = "Mozilla/5.0 \u7565";
UserAgentEnum["SP"] = "Mozilla /5.0 (iPhone;\u7565";
})(UserAgentEnum = TenCasper.UserAgentEnum || (TenCasper.UserAgentEnum = {}));
さきほどのランダムの取得もシンプルになりました。
export function getRandomStringEnum(an:any) {
var len = Object.keys(an).length
var x = Math.floor(Math.random() * len)
var key = <any>Object.keys(an)[x]
target = an[key]
return target
}
まとめ
Typescriptで プログラムができるようになり、以下のメリットがありました。
- 型システムの恩恵により、ミスペルなどのケアレスミスの軽減
- enumをランダムで回すテストによる、網羅率の向上
- 動線でのキャプションの一貫性の無さ、css-selectorの命名の違いの検出
- SAN値の低下を防ぐ
最後に
アスタミューゼでは現在、エンジニア・デザイナーを募集中です。 興味のある方はぜひ下記バナーからご応募ください。