-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathtest-double-phpunit.html
209 lines (151 loc) · 8.14 KB
/
test-double-phpunit.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8' />
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<meta name="description" content="Akkunchoi.github.com : " />
<link rel="stylesheet" type="text/css" media="screen" href="/stylesheets/rainbow-github.css">
<link rel="stylesheet" type="text/css" media="screen" href="/stylesheets/stylesheet.css">
<title>Test DoubleとPHPUnit | akkunchoi@github</title>
<link rel="alternate" type="application/rss+xml" title="RSS Feed for mysite.com" href="/feed.xml" />
<script type="text/javascript" src="/javascripts/rainbow-custom.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script type="text/javascript">
$(document).ready(function($){
//$('pre > code').highlight({source: 1, zebra: 1, indent: 'space', list: 'ol'});
});
</script>
</head>
<body>
<!-- HEADER -->
<div id="header_wrap" class="outer">
<header class="inner">
<a id="forkme_banner" href="https://github.com/akkunchoi">View on GitHub</a>
<h1 id="project_title">
<a href="/">akkunchoi.github.com</a>
</h1>
<h2 id="project_tagline"></h2>
</header>
</div>
<!-- MAIN CONTENT -->
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
<article class="posts">
<header>
<h1>Test DoubleとPHPUnit</h1>
<ul class="tags">
<li class="inline archive_list"><a class="tag_list_link" href="/tag/xUnit">xUnit</a></li>
</ul>
</header>
<div class="posts-content">
<h2 id="test-double">Test Doubleについて</h2>
<p>単体テストを書く時に問題になるのが、依存コンポーネントの扱いだ。例えばデータベースが必要だったり、外部サービスへの接続が必要だったりすると、テストごとに毎回セットアップしていたら途方も無い時間がかかるし、そもそも依存関係が複雑でテストができない場合もある。</p>
<p>このような場合に、実際のオブジェクトではなく代役(Double)のオブジェクトを使うのが Test Doubleというテスト技法だ。Test Doubleにはスタブ、モック、フェイク、ダミーなど様々な手法がある。</p>
<p>特に重要で間違いやすいのが「モック」と「スタブ」だ。英単語では「モック」は模造品、「スタブ」は切り株、切り残しという意味だが、Test Doubleにおいては「モック」は出力の確認、「スタブ」は入力の差し替えを意味する。</p>
<p><a href="http://xunitpatterns.com/Test%20Double.html">xUnit Patterns</a> にTest Doubleのより詳しい解説がある。Test Doubleを使用する理由は、テスト対象オブジェクト(SUT: System Under Test)が、あるコンポーネントに依存(DOC: depended-on component)するために、「 出力を確認できない」「 入力を設定できない」「 遅い」という原因によりテストが困難になるからである。</p>
<h2 id="phpunit">PHPUnitの例</h2>
<p>例として、CMS的なシステムを考える。テスト対象オブジェクト Page は作成者を意味する User と永続化を行う Database に依存している。</p>
<p>テスト対象オブジェクト</p>
<pre><code>class Page{
protected $user;
protected $title;
protected $body;
protected $database;
public function setUser($user) {
$this->user = $user;
}
public function setTitle($title) {
$this->title = $title;
}
public function setBody($body) {
$this->body = $body;
}
public function setDatabase(Database $database){
$this->database = $database;
}
public function save(){
$s = sprintf(
'%s wrote: "%s" %s', $this->user->getName(), $this->title, $this->body
);
$this->database->write($this->title, $s);
}
}
</code></pre>
<p>依存コンポーネントの Database と User</p>
<pre><code>interface Database{
public function write();
}
interface User{
public function getName();
}
</code></pre>
<p>Pageのsaveメソッドをテストするには Database と User が必要である。しかし、Pageをテストするためだけなのに、わざわざ Database や User をインスタンス化する必要は全くない。User::getNameでダミーの値を返すようにしたり、 Database::write メソッドで何が与えられたか確認するだけで十分だ。</p>
<p>PHPUnitにはモックとスタブの生成機能がある。どちらも getMock というメソッドで生成できる。テストコードはこのようになる。</p>
<pre><code>class PageTest extends PHPUnit_Framework_TestCase{
public function testSave(){
// モック
$dbMock = $this->getMock('Database');
$dbMock->expects($this->once())
->method('write')
->with($this->equalTo('New Title'), $this->equalTo('aaa wrote: "New Title" New Text'));
// スタブ
$userStub = $this->getMock('User');
$userStub->expects($this->any())
->method('getName')
->will($this->returnValue('aaa'));
// Exercise
$page = new Page();
$page->setDatabase($dbMock);
$page->setUser($userStub);
$page->setBody('New Text');
$page->setTitle('New Title');
// Verify
$page->save();
}
}
</code></pre>
<p>getMock メソッドによりモックを生成する。普通のクラスだけでなく、インタフェースでも抽象クラスでも良い。オプションでコンストラクタを実行するかどうかも制御できる。</p>
<pre><code>$dbMock = $this->getMock('Database');
</code></pre>
<p>expects メソッドでどのような動作をするかをの設定を開始し、引数に何度呼ばれるべきかを設定する。<code>$this->once()</code>の他には any, never, atLeastOnce, once, exactly, at が利用できる。スタブとして生成するならコール回数の確認を行わない <code>$this->any()</code> にする。</p>
<p>method メソッドでどのメソッドについての動作か指定する。</p>
<p>with メソッドでexpectation を設定し、will メソッドで戻り値を設定する。</p>
<pre><code>$dbMock->expects($this->once())
->method('write')
->with($this->equalTo('New Title'), $this->equalTo('aaa wrote: "New Title" New Text'));
</code></pre>
<p>このように、依存コンポーネントのimplementが存在しないにもかかわらず、対象オブジェクトがテストできるというのはとても強力だ。</p>
<p>しかしPHPUnitのモック生成機能はわかりにくい(覚えにくい)。</p>
<p>他にもSimpleTestがモック生成機能をサポートしていたり、Mockery, Phakeといったモック生成ライブラリがあるらしいので、そのうち調べてみようと思う。</p>
<h2 id="section">参考</h2>
<ul>
<li><a href="http://capsctrl.que.jp/kdmsnr/wiki/bliki/?TestDouble">bliki</a></li>
<li><a href="http://xunitpatterns.com/Test%20Double.html">xUnit Patterns</a></li>
<li><a href="http://www.phpunit.de/manual/3.6/ja/test-doubles.html">PHPUnit</a></li>
</ul>
</div>
<footer>
<p>Last updated at <time>2012-08-07 22:14:31 +0900</time></p>
<!-- <p>Published at <time>2012-07-28</time></p> -->
</footer>
</article>
</section>
</div>
<!-- FOOTER -->
<div id="footer_wrap" class="outer">
<footer class="inner">
<p>Published with <a href="http://pages.github.com">GitHub Pages</a></p>
</footer>
</div>
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-36469923-5']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</body>
</html>