-
Notifications
You must be signed in to change notification settings - Fork 0
/
ch02-model-tdd.xml
415 lines (326 loc) · 15.1 KB
/
ch02-model-tdd.xml
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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
<?xml version="1.0" encoding="UTF-8"?>
<!--
<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
"http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
-->
<!-- =================================================================== -->
<sect1 id="psm.tdd">
<title>模型的敏捷开发</title>
<para><emphasis>忘记Web吧:</emphasis></para>
<para>我们要开发出一套Web应用,但首先要忘掉Web。这看似矛盾,却正是MVC的要求和精髓。
即对核心算法进行抽象,先实现 <literal>Model</literal>,之后再去考虑
<literal>Controller</literal>(控制器)和
<literal>View</literal>(Web展现)。</para>
<para><emphasis>忘记详细设计吧:</emphasis></para>
<para>敏捷开发,可不要等到图纸都出来再按图索骥。而是一种小步快跑的开发模式,
将我们伟大的目标分解为一个一个小的目标,小到能够在一天之内就可以完成。</para>
<para><emphasis>先从测试做起:</emphasis></para>
<para>敏捷开发的一种是测试先行,让我们在第一个迭代中基于一个最简单的目标:实现单元测试框架。</para>
<!-- ================================================================= -->
<sect2 id="psm.tdd.iter1">
<title>迭代1:测试框架的建立</title>
<para>首先搭建单元测试框架,并完成一个最小的功能集合。</para>
<!-- =============================================================== -->
<sect3 id="psm.tdd.iter1.goal">
<title>假想任务目标</title>
<para>首先为我们的模型起个名字:<filename>svnauthz</filename>。</para>
<para>Subversion路径授权中,用户对象(用户/别名/组)显然是最重要的基本单位,
每一条授权策略都包含一个用户对象。那么我们第一个迭代就实现用户对象:
<classname>User</classname> 类,<classname>Alias</classname> 类,
<classname>Group</classname> 类。</para>
<para>假设 <package>svnauthz</package> 的 <classname>User</classname>,
<classname>Alias</classname>, <classname>Group</classname> 类已经完成,
我们期望他们实现的功能是什么呢?于是在纸上写下假想任务目标(模拟python交互式命令行):</para>
<programlisting>
>>> <emphasis>from svnauthz import User, Group, Alias</emphasis>
>>> <emphasis>user1=User('Tom')</emphasis>
>>> <emphasis>user2=User("Jerry")</emphasis>
>>> <emphasis>print user1</emphasis>
Tom # 显示 user1 内容(字符串化)
>>> <emphasis>alias1=Alias('admin')</emphasis>
>>> <emphasis>alias1.user = user1</emphasis>
>>> <emphasis>print alias1</emphasis>
admin = Tom # 显示 alias1 内容(字符串化)
>>> <emphasis>group1 = Group('team1')</emphasis>
>>> <emphasis>group2 = Group('team2')</emphasis>
>>> <emphasis>group1.append(group2, user2, alias1, user1)</emphasis>
>>> <emphasis>print group1</emphasis>
team1 = &admin, @team2, Jerry, Tom # group1 的成员列表要进行排序
>>> <emphasis>group2.append(group1, user1)</emphasis>
Exception: ... # 抛出异常! group1 引起了组间的循环引用
>>> <emphasis>group2.append(group1, user1, autodrop=True)</emphasis>
>>> <emphasis>print group2</emphasis>
team2 = Tom # 使用 autodrop 参数,自动抛弃冲突的组成员,而不引发异常。(即容错性)
</programlisting>
</sect3>
<!-- =============================================================== -->
<sect3 id="psm.tdd.iter1.unittest.failed">
<title>建立测试用例</title>
<para>将假想的任务目标翻译为测试用例。建立单元测试文件 <filename>test_svnauthz.py</filename> 如下:</para>
<programlisting><![CDATA[
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import unittest
from svnauthz import *
class TestStage1(unittest.TestCase):
def testUser(self):
user1 = User('Tom')
self.assert_(str(user1) == 'Tom')
def testAlias(self):
user1 = User('Tom')
alias1=Alias('admin')
alias1.user = user1
self.assert_(str(alias1) == 'admin = Tom', str(alias1))
def testGroup(self):
user1 = User('Tom')
user2 = User('Jerry')
alias1=Alias('admin')
alias1.user = user1
group1 = Group('team1')
group2 = Group('team2')
group1.append(group2, user2, alias1, user1)
self.assert_(str(group1) == 'team1 = &admin, @team2, Jerry, Tom')
self.assertRaises(Exception, group2.append, group1, user1)
group2.append(group1, user1, autodrop=True)
self.assert_(str(group2) == 'team2 = Tom')
if __name__ == '__main__': unittest.main()
]]></programlisting>
<para>执行测试用例:</para>
<screen>
$ <emphasis>python test_svnauthz.py</emphasis>
Traceback (most recent call last):
File "test_svnauthz.py", line 8, in <module>
from svnauthz import *
ImportError: No module named svnauthz
</screen>
<para>测试失败!不要紧,因为我们还没有写代码呢。</para>
</sect3>
<!-- =============================================================== -->
<sect3 id="psm.tdd.iter1.unittest.pass">
<title>编写模组,使测试用例通过</title>
<para>之前执行测试用例失败,报告:找不到 <classname>svnauthz</classname> 模组。因为模组还没有创建,当然找不到了。
于是创建一个空的模组文件 <filename>svnauthz.py</filename>。</para>
<screen>
$ <emphasis>touch svnauthz.py</emphasis>
</screen>
<para>执行测试用例:</para>
<screen>
$ <emphasis>python test_svnauthz.py</emphasis>
EEE
======================================================================
ERROR: testAlias (__main__.TestStage1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_svnauthz.py", line 17, in testAlias
user1 = User('Tom')
NameError: global name 'User' is not defined
...
</screen>
<para>太棒了,我们前进了一步,因为失败的原因已经不同了。错误报告说:
<classname>User</classname>类未定义。于是我们写一些代码,
让测试用例通过。</para>
<para><filename>svnauthz.py</filename> 第一个版本的代码如下:</para>
<programlisting><![CDATA[
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 """Subversion authz config file management.
5
6 Basic classes used for Subversion authz management.
7 """
8
9 class User(object):
10
11 def __init__(self, name):
12 name = name.strip()
13
14 if not name:
15 raise Exception, 'Username is not provided'
16
17 self.__name = name
18
19 def __str__(self):
20 return self.__name
]]></programlisting>
<para>再次执行测试用例:</para>
<screen>
$ <emphasis>python test_svnauthz.py -v</emphasis>
testAlias (__main__.TestStage1) ... ERROR
testGroup (__main__.TestStage1) ... ERROR
testUser (__main__.TestStage1) ... ok
======================================================================
ERROR: testAlias (__main__.TestStage1)
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_svnauthz.py", line 18, in testAlias
alias1=Alias('admin')
NameError: global name 'Alias' is not defined
...
</screen>
<para>好的,我们已经有一个测试用例(<classname>testUser</classname>)通过了!
其他的测试用例呢?先把他们注释掉,以便提前感受一下完全通过测试的滋味。</para>
<para>注意:我所说的注释掉不是删除代码,也不是把每一行变为注释,
而是非常简单的将暂不考虑的测试用例改名。</para>
<itemizedlist>
<listitem>
<para>将 <code>def testAlias(self)</code> 改为 <code>def _testAlias(self)</code></para>
</listitem>
<listitem>
<para>将 <code>def testGroup(self)</code> 改为 <code>def _testGroup(self)</code></para>
</listitem>
</itemizedlist>
<note>
<para>注:只要不是以 <literal>test</literal> 开头都好。</para>
</note>
<para>再次执行测试用例,太棒了完全通过!</para>
<screen>
$ <emphasis>python test_svnauthz.py -v</emphasis>
testUser (__main__.TestStage1) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
</screen>
</sect3>
<!-- =============================================================== -->
<sect3 id="psm.tdd.iter1.code.coverage">
<title>完善测试用例</title>
<para>检查代码覆盖度,在 Python 下有 <package>coverage</package> 包可用。
用 <command>easy_install</command> 安装之后,
就可以使用 <command>coverage</command> 命令了。</para>
<screen>
$ <emphasis>coverage -x test_svnauthz.py</emphasis>
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
$ ls .coverage
.coverage
$ coverage -r -m svnauthz.py
Name Stmts Exec Cover Missing
----------------------------------------
svnauthz 8 7 87% 15
</screen>
<para>哦,看来我们离完美还是差了一点。从 <command>coverage</command>
的输出中可以看出,我们的测试用例并没有对 <filename>svnauthz.py</filename>
的代码测试完全:第15行没有测试到。也就是用<emphasis>空的用户名</emphasis>创建
<classname>User</classname> 对象,应该抛出异常。</para>
<para>我们在 <methodname>testUser</methodname> 用例的最后补充一条断言:</para>
<programlisting>
def testUser(self):
user1 = User('Tom')
self.assert_(str(user1) == 'Tom')
<emphasis>self.assertRaises(Exception, User, " ")</emphasis>
</programlisting>
<para>再次检查一下测试用例对代码的覆盖度。哇,100% 通过!</para>
<screen>
$ <emphasis>coverage -x test_svnauthz.py</emphasis>
.
----------------------------------------------------------------------
Ran 1 test in 0.002s
OK
$ <emphasis>coverage -r -m svnauthz.py</emphasis>
Name Stmts Exec Cover Missing
----------------------------------------
svnauthz 8 8 100%
</screen>
</sect3>
<!-- =============================================================== -->
<sect3 id="psm.tdd.iter1.nosetests">
<title>用例管理和 nosetests</title>
<para>目前来讲,代码和测试用例共存于同一个目录。我们重构一下,将模组代码放在
<filename>src</filename> 目录,将测试用例放在 <filename>tests</filename>
目录。</para>
<para>执行测试用例:</para>
<screen>
$ <emphasis>python tests/test_svnauthz.py</emphasis>
Traceback (most recent call last):
File "tests/test_svnauthz.py", line 8, in <module>
from svnauthz import *
ImportError: No module named svnauthz
</screen>
<para>在 <filename>test_svnauthz.py</filename> 文件头增加如下语句,
设置 Python 模组查询路径:</para>
<programlisting>
import sys
sys.path.insert(0,'src')
</programlisting>
<para>测试用例又可以成功执行了。</para>
<para>目录 <filename>tests</filename> 下如果有多个测试用例文件,
难道要一个一个去调用么?或者用 <classname>unittest.TestSuite</classname>
去组织测试用例?其实不用这么麻烦,<application>nosetests</application>
可以自动发现目录下的测试用例,并执行。</para>
<para>鼻子测试(<application>nosetests</application>)是一个主动发现测试用例的
unittest 扩展。可以用 <command>easy_install</command> 来安装:</para>
<screen>
$ <emphasis>easy_install nose</emphasis>
$ <emphasis>nosetests</emphasis>
.
----------------------------------------------------------------------
Ran 1 test in 0.008s
OK
</screen>
<para>代码覆盖度测试</para>
<screen>
$ <emphasis>nosetests --with-coverage --cover-package=svnauthz</emphasis>
.
Name Stmts Exec Cover Missing
----------------------------------------
svnauthz 8 8 100%
----------------------------------------------------------------------
Ran 1 test in 0.030s
OK
</screen>
</sect3>
</sect2>
<!-- ================================================================= -->
<sect2 id="psm.tdd.continued">
<title>持续迭代</title>
<para>持续迭代,完成 <classname>User</classname>, <classname>Group</classname>,
<classname>Alias</classname>, <classname>Rules</classname>,
<classname>Module</classname>, <classname>Repos</classname>,
<classname>SvnAuthz</classname> 等模组。</para>
</sect2>
<!-- ================================================================= -->
<sect2 id="psm.tdd.final">
<title>最终完成的 svnauthz</title>
<para>在 Python 交互模式下测试 <classname>svnauthz</classname> 模组:</para>
<screen>
>>> <emphasis>buff = '''# admin: / = administrator</emphasis>
... <emphasis>[groups]</emphasis>
... <emphasis>group1=user1,user2</emphasis>
... <emphasis>[/]</emphasis>
... <emphasis>$authenticated=r</emphasis>
... <emphasis>[/trunk]</emphasis>
... <emphasis>@group1 = r</emphasis>
... <emphasis>user3 = rw'''</emphasis>
>>> <emphasis>import StringIO</emphasis>
>>> <emphasis>file = StringIO.StringIO(buff)</emphasis>
>>> <emphasis>authz=SvnAuthz()</emphasis>
>>> <emphasis>authz.load(file)</emphasis>
>>> <emphasis>[x.name for x in authz.reposlist]</emphasis>
['/']
>>> <emphasis>[x.uname for x in authz.userlist]</emphasis>
[u'administrator', u'user1', u'user2', u'user3']
>>> <emphasis>[x.uname for x in authz.userlist]</emphasis>
[u'administrator', u'user1', u'user2', u'user3']
>>> <emphasis>[x.uname for x in authz.grouplist]</emphasis>
[u'@group1', u'$authenticated']
>>> <emphasis>[x.uname for x in authz.aliaslist]</emphasis>
[]
>>> <emphasis>print authz.grouplist</emphasis>
[groups]
group1 = user1, user2
>>> <emphasis>print authz.aliaslist</emphasis>
[aliases]
>>> <emphasis>authz.is_admin('administrator','/')</emphasis>
True
>>> <emphasis>authz.is_admin('administrator','repos1')</emphasis>
True
>>> <emphasis>authz.add_rules('/', '/trunk', '&admin=rw; $authenticated=')</emphasis>
>>> <emphasis>module1 = authz.get_module('/', '/trunk')</emphasis>
>>> <emphasis>[str(x) for x in module1]</emphasis>
['@group1 = r', 'user3 = rw', '$authenticated = ', '&admin = rw']
</screen>
<para>现在是时候给 <classname>svnauthz</classname> 套上一个华丽一点的外衣了。</para>
</sect2>
</sect1>