フロントエンドJavaScriptにおける
※ この話は今日はしない
※ 今日出てくる「設計」はこっちの意味
JavaScriptによるアプリケーションの設計で重要なのはこの二つ
$(function() {
// 要素取得してー
var $form = $('.todoForm');
var $input = $form.find('input[type="text"]');
var $list = $('.todoList');
// フォームがサブミットされたらー
$form.submit(function(e) {
e.preventDefault();
// 要素作って追加ー
var text = $input.val();
var html = '<li><input type="checkbox">' + text + '</li>';
var $li = $(html);
$li.find('input[type="checkbox"]').change(function() {
$(this).closest('li').toggleClass('complete');
});
$list.append($li);
});
});
よく使うTodoを用意してクリック
で入力できるようしてー
$(function() {
var $form = $('.todoForm');
var $input = $form.find('input[type="text"]');
var $list = $('.todoList');
var $usual = $('.usualList li');
// 共通の処理(リストを追加する部分)を関数に切り出した
function addList(text) {
var html = '<li><input type="checkbox">' + text + '</li>';
var $li = $(html);
$li.find('input[type="checkbox"]').change(function() {
$(this).closest('li').toggleClass('complete');
});
$list.append($li);
}
// よく使う一覧をクリックしたらリストに追加
$usual.click(function(e) {
e.preventDefault();
var text = $(this).text();
addList(text);
});
// フォームをサブミットしたらリストに追加
$form.submit(function(e) {
e.preventDefault();
var text = $input.val();
addList(text);
});
});
// Todoのデータ管理するModelクラス
function Todo(data) {
this.text = data.text;
this.complete = !!data.complete;
}
// 説明簡略化のためBackbone.Eventから拝借したEventをmixin
// onとかtriggerメソッドが使えるようになる
$.extend(Todo.prototype, Events);
$.extend(Todo, Events);
// completeプロパティを変更するメソッド
Todo.prototype.setComplete = function(complete) {
this.complete = !!complete;
this.trigger('change:complete', this);
};
// 自身のインスタンスを保持する配列
Todo.list = [];
// 新規Todoを追加するためのクラスメソッド
Todo.add = function(text) {
var todo = new Todo({ text: text });
Todo.list.push(todo);
this.trigger('add', todo);
};
// Todoを入力するフォームを管理するViewクラス
function TodoFormView($el) {
this.$el = $el;
this.$input = this.$el.find('input[type="text"]');
this.$el.submit(this.onsubmit.bind(this));
}
// サブミット時のイベントハンドラ
TodoFormView.prototype.onsubmit = function(e) {
e.preventDefault();
Todo.add(this.$input.val());
};
// Todo一覧のリストを管理するViewクラス
function TodoListView($el) {
this.$el = $el;
Todo.on('add', this.add.bind(this));
}
// Todoの要素を追加するメソッド
TodoListView.prototype.add = function(todo) {
var item = new TodoListItemView(todo);
this.$el.append(item.$el);
};
// Todo一覧の要素をを管理するViewクラス
function TodoListItemView(todo) {
this.todo = todo;
this.$el = $('<li><input type="checkbox">' + todo.text + '</li>');
this.$checkbox = this.$el.find('input[type="checkbox"]');
this.$checkbox.change(this.onchangeCheckbox.bind(this));
this.todo.on('change:complete', this.onchangeComplete.bind(this));
}
// checkboxの値が変わった時のイベントハンドラ
TodoListItemView.prototype.onchangeCheckbox = function() {
this.todo.setComplete(this.$checkbox.is(':checked'));
};
// モデルのcompleteプロパティの値が変わった時のイベントハンドラ
TodoListItemView.prototype.onchangeComplete = function() {
if (this.todo.complete) {
this.$el.addClass('complete');
}
else {
this.$el.removeClass('complete');
}
this.$checkbox.attr('checked', this.todo.complete);
};
最後にViewを初期化
$(function() {
new TodoFormView( $('.todoForm') );
new TodoListView( $('.todoList') );
});
よく使うTodoを用意してクリック
で入力できるようしてー
$('.usualList li').click(function() {
Todo.add($(this).text());
});
※ 必要に応じてViewクラスにしてね
全部完了にするボタンつけてー
Todo.setCompleteAll = function() {
Todo.list.forEach(function(todo) { todo.setComplete(true); });
};
$('.completeAll').click(function() {
Todo.setCompleteAll();
});
※ 必要に応じてViewクラスにしてね
きちんと設計するとハッピーになれる
var casper = require('casper').create();
casper.start('./index.html', function() {
this.evaluate(function() {
var form = document.querySelector('.todoForm');
form.querySelector('input[type="text"]').value = 'foo';
form.querySelector('input[type="submit"]').click();
});
});
casper.then(function() {
this.test.assertEvalEquals(function() {
return document.querySelectorAll('.todoList li').length;
}, 1, 'Added Todo List');
this.test.assertEvalEquals(function() {
return document.querySelector('.todoList li').textContent;
}, 'foo', 'Added input value');
});
casper.run(function() {
this.test.done();
this.test.renderResults(true);
});
実行するとFirefoxが起動してテストする
require 'test/unit'
require 'selenium-webdriver'
class TodoAppTest < Test::Unit::TestCase
def setup
@driver = Selenium::WebDriver.for :firefox
end
def teardown
@driver.quit
end
def test_submit_todo
url = "file://#{File.expand_path('..', __FILE__)}/todo/index.html"
@driver.navigate.to url
input = @driver.find_element :name => 'text'
input.send_keys 'foo'
input.submit
list = @driver.find_elements :css => '.todoList li'
assert_equal(list.size, 1)
assert_equal(list[0].text, 'foo')
end
end
基本はこれだけ
describe('テストの対象', function() {
it('テストの内容', function() {
// ここで例外が投げられたらテストが落ちる
});
it('テストの内容', function() {
// ここで例外が投げられたらテストが落ちる
});
});
describe('テストの対象', function() {
// describeはネストできる
describe('テストの対象', function() {
it('テストの内容', function() {
// ...
});
});
describe('テストの対象', function() {
it('テストの内容', function() {
// ...
});
});
});
describe('テストの対象', function() {
// テストの事前処理
before(function() {
});
// テストの事後処理
after(function() {
});
// itの前に毎回行う処理
beforeEach(function() {
});
// itの後に毎回行う処理
afterEach(function() {
});
});
describe('テストの対象', function() {
// doneを引数に取ると非同期
it('非同期のテスト', function(done) {
setTimeout(function() {
// ここにテストを書く
done();
}, 100);
});
});
現状ほぼこの二つのどちらかしかない
条件に合わないと例外を投げる
expect(foo).to.be('bar');
expect(foo).to.eql({ foo: 'bar' });
expect(foo).to.have.property('bar', 'baz');
expect(foo).to.be.a(Date);
expect(function() {
foo();
}).to.throwError();
describe('Todo', function() {
describe('.add', function() {
it('Todo.listにインスタンスが追加されること', function() {
Todo.add({ text: 'foo' });
expect(Todo.list).to.have.length(1);
expect(Todo.list[0]).to.be.a(Todo);
});
});
describe('#setComplete', function() {
var todo;
beforeEach(function() {
todo = new Todo({});
});
it('completeが設定されること', function() {
todo.setComplete(true);
expect(todo.complete).to.be(true);
});
it('change:completeイベントが発火すること', function(done) {
todo.on('change:complete', function() {
expect(todo.complete).to.be(true);
done();
});
todo.setComplete(true);
});
});
});
テストダブル (Test Double) とは、ソフトウェアテストにおいて、テスト対象が依存しているコンポーネントを置き換える代用品のこと。ダブルは代役、影武者を意味する。
テストダブル - Wikipedia
// Todo.addにspyを忍ばせる
var spy = sinon.spy(Todo, 'add');
// Todo.addメソッドを実行
Todo.add('foo');
// メソッドが呼ばれたかどうかや引数を調べられる
expect(spy.calledOnce).to.ok();
expect(spy.args[0][0]).to.be('foo');
// spyを解除
spy.restore();
ListView.prototype.remove = function() {
if (window.confirm('削除しますか?')) {
this.$el.remove();
this.$el = null;
}
};
テストのたびにconfirmが出る
(しかも選択によってテストの成否が変わる)
describe('ListView', function() {
var listView;
beforeEach(function() {
listView = new ListView($('<ul>'));
});
describe('#remove', function() {
it('要素が削除されること', function() {
listView.remove();
expect(listView.$el).to.be(null);
});
});
});
ダイアログがでない!結果
describe('ListView', function() {
describe('#remove', function() {
var stub;
var listView;
beforeEach(function() {
stub = sinon.stub(window, 'confirm');
listView = new ListView($('<ul>'));
});
afterEach(function() {
stub.restore();
});
it('window.confirmが呼ばれること', function() {
listView.remove();
expect(stub.calledOnce).to.ok();
expect(stub.args[0][0]).to.be('削除しますか?');
});
context('window.confirmでOKを押したとき', function() {
beforeEach(function() {
stub.returns(true);
});
it('要素が削除されること', function() {
listView.remove();
expect(listView.$el).to.be(null);
});
});
context('window.confirmでキャンセルを押した時', function() {
beforeEach(function() {
stub.returns(false);
});
it('要素が削除されないこと', function() {
listView.remove();
expect(listView.$el).to.not.be(null);
});
});
});
});
describe('TodoFormView', function() {
var todoForm;
var html = '<form><input type="text"></form>';
beforeEach(function() {
todoForm = new TodoFormView($(html));
});
it('$elに要素がセットされていること', function() {
expect(todoForm.$el.is('form')).to.ok();
});
context('submitしたとき', function() {
var spy;
beforeEach(function() {
spy = sinon.spy(Todo, 'add');
todoForm.$input.val('foo');
todoForm.$el.submit();
});
afterEach(function() {
spy.restore();
});
it('textの値がTodo.addに渡されること', function() {
expect(spy.calledOnce).to.ok();
expect(spy.args[0][0]).to.be('foo');
});
});
});
dispatchEventでイベントを発火させて動くか確認する
// jQueryのtriggerのようなもの
function trigger(element, eventType, params) {
var ev = document.createEvent('Event');
ev.initEvent(eventType, true, false);
$.extend(ev, params || {});
element.dispatchEvent(ev);
}
// タッチイベントを発火させて現在地が動くかをテストする
it('should move to next', function() {
trigger(f.element, touchStartEvent, { pageX: 50, pageY: 0 });
expect(f.currentPoint).to.be(0);
trigger(f.element, touchMoveEvent, { pageX: 40, pageY: 0 });
trigger(f.element, touchMoveEvent, { pageX: 30, pageY: 0 });
expect(f.currentPoint).to.be(0);
trigger(document, touchEndEvent);
expect(f.currentPoint).to.be(1);
});