Using phpunit to practice TDD series
Start with a bank account
Assume you have phpunit installed.
We start with a simple bank account example to understand the idea of TDD (Test-Driven-Development).
Create two directories in the project directory, src
and test
, create the file BankAccount.php
under src
, and create the file BankAccountTest.php
under the test
directory.
According to the idea of TDD, we write the test first and then write the production code, so BankAccount.php
is left blank and we write BankAccountTest.php
first.
<code><?php class BankAccountTest extends PHPUnit_Framework_TestCase { } ?></code>
Now let’s run it and see the results. The command line to run phpunit is as follows:
<code>phpunit --bootstrap src/BankAccount.php test/BankAccountTest.php</code>
--bootstrap src/BankAccount.php
means to load src/BankAccount.php
before running the test code. The test code to be run is test/BankAccountTest.php
.
If you do not specify a specific test file and only provide a directory, phpunit will run all files in the directory whose file names match *Test.php
. Because there is only one file BankAccountTest.php
in the test
directory, so execute
<code>phpunit --bootstrap src/BankAccount.php test</code>
You will get the same result.
<code>There was 1 failure: 1) Warning No tests found in class "BankAccountTest". FAILURES! Tests: 1, Assertions: 0, Failures: 1.</code>
A warning error since there aren't any tests.
Account instantiation
Let’s add a test below. Note that TDD is a design method that can help you design the functionality of a module from the bottom up. When we write tests, we must start from the user's perspective. If a user uses our BankAccount
class, what does he do first? It must be a new instance of BankAccount. So our first test is the test of instantiation.
<code>public function testNewAccount(){ $account1 = new BankAccount(); }</code>
Running phpunit failed as expected.
<code>PHP Fatal error: Class 'BankAccount' not found in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 5</code>
No definition of the BankAccount
class was found. Next we will write the production code. Make the test pass. Enter the following content in src/BankAccount.php
(hereinafter referred to as the source file):
<code><?php class BankAccount { } ?></code>
Run phpunit and the test passes.
<code>OK (1 test, 0 assertions)</code>
Next, we need to add tests to make the tests fail. If you create a new account, the balance of the account should be 0. So we added an assert
statement:
<code>public function testNewAccount(){ $account1 = new BankAccount(); $this->assertEquals(0, $account1->value()); }</code>
Note that value()
is a member function of BankAccount
. Of course, this function has not been defined yet. As users, we hope BankAccount
provides this function.
Run phpunit, the results are as follows:
<code>PHP Fatal error: Call to undefined method BankAccount::value() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 6</code>
The result tells us that BankAccount
does not have the value()
member function. Add production code:
<code>class BankAccount { public function value(){ return 0; } }</code>
Why should value()
return 0 directly? Because the test code expects value()
to return 0. The principle of TDD is not to write redundant production code, just enough to allow the test to pass.
Account deposit and withdrawal
After running phpunit and passing, we first assume that the instantiation of BankAccount
has met the requirements. Next, how does the user want to use BankAccount
? You definitely want to deposit money into it. Well, I hope BankAccount
has a deposit function. By calling this function, you can increase the account balance. So we add the next test.
<code>public function testDeposit(){ $account = new BankAccount(); $account->deposit(10); $this->assertEquals(10, $account->value()); }</code>
The initial balance of the account is 0. If we deposit 10 yuan into it, the account balance should of course be 10. Running phpunit, the test fails because the deposit function is not defined yet:
<code>.PHP Fatal error: Call to undefined method BankAccount::deposit() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 11</code>
Next add the deposit function to the source file:
<code>public function deposit($ammount) { }</code>
Run phpunit again and get the following results:
<code>1) BankAccountTest::testDeposit Failed asserting that 0 matches expected 10.</code>
At this time, because we did not operate the account balance in the deposit function, the initial value of the balance is 0, and it is still 0 after the deposit function is executed, which is not the behavior expected by the user. We should add the amount deposited by the user to the balance.
In order to operate the balance, the balance should be a member variable of BankAccount. This variable is not allowed to be changed by the outside world, so it is defined as a private variable. Next we add the private variable $value
to the production code, then the value
function should return the value of $value
.
<code>class BankAccount { private $value; public function value(){ return $this->value; } public function deposit($ammount) { $this->value = 10; } }</code>
Run phpunit and the test passes. Next, we thought, what else do users need? Yes, withdraw money. When withdrawing money, this value is deducted from the account balance. If you pass a negative number to the deposit
function, it is equivalent to withdrawing money.
So we add two lines of code to the testDeposit
function of the test code.
<code>$account->deposit(-5); $this->assertEquals(5, $account->value());</code>
Run phpunit again and the test failed.
<code>1) BankAccountTest::testDeposit Failed asserting that 10 matches expected 5.</code>
This is because in the production code we simply set $value
to the result of 10. Improve production code.
<code>public function deposit($ammount) { $this->value += $ammount; }</code>
Run phpunit again and the test passes.
New constructor
Next, it occurred to me that the user might need a different constructor that could pass in a value as the account balance when creating the BankAccount
object. So we add this instantiation test in testNewAccount
.
<code>public function testNewAccount(){ $account1 = new BankAccount(); $this->assertEquals(0, $account1->value()); $account2 = new BankAccount(10); $this->assertEquals(10, $account2->value()); }</code>
Run phpunit, the result is:
<code>1) BankAccountTest::testNewAccount Failed asserting that null matches expected 10.</code>
这时因为BankAccount
没有带参数的构造函数,因此new BankAccount(10)
会返回一个空对象,空对象的value()
函数自然返回的也是null。为了通过测试,我们在生产代码中增加带参数的构造函数。
<code>public function __construct($n){ $this->value = $n; }</code>
再运行测试:
<code>1) BankAccountTest::testNewAccount Missing argument 1 for BankAccount::__construct(), called in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 5 and defined /home/wuchen/projects/jolly-code-snippets/php/phpunit/src/BankAccount.php:5 /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php:5 2) BankAccountTest::testDeposit Missing argument 1 for BankAccount::__construct(), called in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 12 and defined /home/wuchen/projects/jolly-code-snippets/php/phpunit/src/BankAccount.php:5 /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php:12</code>
两个调用new BankAccount()
的地方都报告了错误,增加了带参数的构造函数,不带参数的构造函数又不行了。从c++/java
过渡来的同学马上想到增加一个默认的构造函数:
<code>public function __construct() { $this->value = 0; }</code>
但这样是不行的,因为php不支持函数重载,所以不能有多个构造函数。
怎么办?对了,我们可以为参数增加默认值。修改构造函数为:
<code>public function __construct($n = 0){ $this->value = $n; }</code>
这样调用 new BankAccount()
时,相当于传递了0给构造函数,满足了需求。
phpunit运行以下,测试通过。
这时,我们的生产代码为:
<code><?php class BankAccount { private $value; // default to 0 public function __construct($n = 0){ $this->value = $n; } public function value(){ return $this->value; } public function deposit($ammount) { $this->value += $ammount; } } ?></code>
总结
虽然我们的代码并不多,但是每一步都写得很有信心,这就是TDD的好处。即使你对php的语法不是很有把握(比如我),也可以对自己的代码很有信心。
用TDD的方式写程序的另一个好处,就是编码之前不需要对单个模块进行仔细的设计,可以在写测试的时候进行设计。这样开发出来的模块既可以满足用户需要,也不会冗余。
后面将会介绍 phpunit 的更多用法。
以上就介绍了用phpUnit入门TDD,包括了方面的内容,希望对PHP教程有兴趣的朋友有所帮助。