awaken

博客园 首页 新随笔 联系 订阅 管理

PHPUnit的介绍可以翻翻前面几篇文章。

本文参考文献:

[Astels2006] A New Look at Test-Driven Development

在[Astels2006] A New Look at Test-Driven Development中,Dave Astels提出如下观点:
*极限编程(Extreme Programming)最初的原则是测试所有可能发生错误的地方。
*现在极限编程的测试实践已经进化到TDD(Test-Driven Development)
*但是工具依然强迫developer考虑测试用例和断言,而不是考虑需求说明。

Dave Astels原文:
So if it's not about testing, what's it about?
It's about figuring out what you are trying to do before you run off half-cocked to try to do it.
You write a specification that nails down a small aspect of behaviour in a concise, unambiguous, and executable form.
It's that simple. Does that mean you write tests? No. It means you write specifications of what your code will have to do.
It means you specify the behaviour of your code ahead of time. But not far ahead of time.
In fact, just before you write the code is best because that's when you have as much information at hand as you will up to that point.
Like well done TDD, you work in tiny increments... specifying one small aspect of behaviour at a time, then implementing it.

When you realize that it's all about specifying behaviour and not writing tests, your point of view shifts.
Suddenly the idea of having a Test class for each of your production classes is ridiculously limiting.
And the thought of testing each of your methods with its own test method (in a 1-1 relationship) will be laughable.

如果关键点不是测试,那是什么呢?
关键是在你马上就要做一件事之前指出你到底要做什么。你写了一份说明,详细到每一个细节的行为,简明、无歧义、可执行。
这如此简单。但那并不意味着你写了tests,你只是写了你的代码的功能说明书。这意味着你在写代码前详细分析了代码的行为。实际上,
等到你马上就要写代码时再去做这件事最好,因为这时你才能掌握关于这件事最详尽的信息。就像做的很好的TDD,你只需要多做一点点工作就OK。
一次分析清楚所有行为细小的方面,然后实现它。
当你意识到问题的关键是分析行为(specifying behaviour)而不是写tests,你的观念发生了变化。突然,每个类对应一个测试类的主意狭隘的可笑。

对你的每一个方法都用相对应的test method(自己的方法和测试脚本的方法数量一比一)去测试?这是个笑话。

 ---Dave Astels

 

行为驱动开发(BDD)的关注点是软件开发过程中的语言和交互。BDD的开发者用他们自己的语言和领域驱动设计(Domain-Driven Design)的普遍语言相结合,
来描述代码的行为和目的。这使得开发者关注代码的目的,而不是技术细节,最小化在代码编程语言和领域设计专家(domain experts)所说的领域语言之间的差异。

PHPUnit_Extensions_Story_TestCase类添加了一个story framework,便于“领域特定语言”(Domain-Specific Language,DSL)为行为驱动开发做定义。
在一个scenario里,given(),when(),then()各自都是一个步骤。and()类似于前置步骤。下面的方法在PHPUnit_Extensions_Story_TestCase中做了抽象声明,
需要实现他们:
    * runGiven(&$world, $action, $arguments)
      ...
    * runWhen(&$world, $action, $arguments)
      ...
    * runThen(&$world, $action, $arguments)
      ...

保龄球游戏例子
这里我们看看一个类,计算保龄球游戏的分数。规则如下:
    *游戏由10场组成
    *每一场player有2次机会来打倒10个瓶
    *每一场的分数=击倒的瓶子数 + strikes的bonus + spares的bonus
    *spare是指player在2次抛球中击倒全部10个瓶子。
     这一场的bonus就是下一次ROLL时击倒的瓶子数。
    *strike是指player第一次就击倒全部10个瓶子。
     这一场的bonus就是下两次ROLL时击倒的瓶子数。

下面看看这些规则怎么表述
代码
  1<?php
  2require_once 'PHPUnit/Extensions/Story/TestCase.php';
  3require_once 'BowlingGame.php';
  4 
  5class BowlingGameSpec extends PHPUnit_Extensions_Story_TestCase
  6{
  7    /**
  8     * @scenario
  9     */
 10    public function scoreForGutterGameIs0()
 11    {
 12        $this->given('New game')
 13             ->then('Score should be', 0);
 14    }
 15 
 16    /**
 17     * @scenario
 18     */
 19    public function scoreForAllOnesIs20()
 20    {
 21        $this->given('New game')
 22             ->when('Player rolls', 1)
 23             ->and('Player rolls', 1)
 24             ->and('Player rolls', 1)
 25             ->and('Player rolls', 1)
 26             ->and('Player rolls', 1)
 27             ->and('Player rolls', 1)
 28             ->and('Player rolls', 1)
 29             ->and('Player rolls', 1)
 30             ->and('Player rolls', 1)
 31             ->and('Player rolls', 1)
 32             ->and('Player rolls', 1)
 33             ->and('Player rolls', 1)
 34             ->and('Player rolls', 1)
 35             ->and('Player rolls', 1)
 36             ->and('Player rolls', 1)
 37             ->and('Player rolls', 1)
 38             ->and('Player rolls', 1)
 39             ->and('Player rolls', 1)
 40             ->and('Player rolls', 1)
 41             ->and('Player rolls', 1)
 42             ->then('Score should be', 20);
 43    }
 44 
 45    /**
 46     * @scenario
 47     */
 48    public function scoreForOneSpareAnd3Is16()
 49    {
 50        $this->given('New game')
 51             ->when('Player rolls', 5)
 52             ->and('Player rolls', 5)
 53             ->and('Player rolls', 3)
 54             ->then('Score should be', 16);
 55    }
 56 
 57    /**
 58     * @scenario
 59     */
 60    public function scoreForOneStrikeAnd3And4Is24()
 61    {
 62        $this->given('New game')
 63             ->when('Player rolls', 10)
 64             ->and('Player rolls', 3)
 65             ->and('Player rolls', 4)
 66             ->then('Score should be', 24);
 67    }
 68 
 69    /**
 70     * @scenario
 71     */
 72    public function scoreForPerfectGameIs300()
 73    {
 74        $this->given('New game')
 75             ->when('Player rolls', 10)
 76             ->and('Player rolls', 10)
 77             ->and('Player rolls', 10)
 78             ->and('Player rolls', 10)
 79             ->and('Player rolls', 10)
 80             ->and('Player rolls', 10)
 81             ->and('Player rolls', 10)
 82             ->and('Player rolls', 10)
 83             ->and('Player rolls', 10)
 84             ->and('Player rolls', 10)
 85             ->and('Player rolls', 10)
 86             ->and('Player rolls', 10)
 87             ->then('Score should be', 300);
 88    }
 89 
 90    public function runGiven(&$world, $action, $arguments)
 91    {
 92        switch($action) {
 93            case 'New game': {
 94                $world['game']  = new BowlingGame;
 95                $world['rolls'= 0;
 96            }
 97            break;
 98 
 99            default: {
100                return $this->notImplemented($action);
101            }
102        }
103    }
104 
105    public function runWhen(&$world, $action, $arguments)
106    {
107        switch($action) {
108            case 'Player rolls': {
109                $world['game']->roll($arguments[0]);
110                $world['rolls']++;
111            }
112            break;
113 
114            default: {
115                return $this->notImplemented($action);
116            }
117        }
118    }
119 
120    public function runThen(&$world, $action, $arguments)
121    {
122        switch($action) {
123            case 'Score should be': {
124                for ($i = $world['rolls']; $i < 20$i++) {
125                    $world['game']->roll(0);
126                }
127 
128                $this->assertEquals($arguments[0], $world['game']->score());
129            }
130            break;
131 
132            default: {
133                return $this->notImplemented($action);
134            }
135        }
136    }
137}
138?>

phpunit --story BowlingGameSpec
PHPUnit 3.4.2 by Sebastian Bergmann.

BowlingGameSpec
 [x] Score for gutter game is 0

   Given New game
    Then Score should be 0

 [x] Score for all ones is 20

   Given New game
    When Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
     and Player rolls 1
    Then Score should be 20

 [x] Score for one spare and 3 is 16

   Given New game
    When Player rolls 5
     and Player rolls 5
     and Player rolls 3
    Then Score should be 16

 [x] Score for one strike and 3 and 4 is 24

   Given New game
    When Player rolls 10
     and Player rolls 3
     and Player rolls 4
    Then Score should be 24

 [x] Score for perfect game is 300

   Given New game
    When Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
     and Player rolls 10
    Then Score should be 300

Scenarios: 5, Failed: 0, Skipped: 0, Incomplete: 0.
posted on 2009-11-27 15:26  awaken  阅读(550)  评论(0)    收藏  举报