Use PHPUnit to combat TDD series
Start with a bank account
Suppose you already have PHPUnit installed.
Let's start with the idea of TDD (test-driven-development) from a simple example of a bank account.
Set up two directories under the project directory, src
and test
, under src
Create files BankAccount.php
, test
create files under directory BankAccountTest.php
.
According to TDD, we write the test first, then write the production code, so we leave it BankAccount.php
blank, we write first BankAccountTest.php
.
Now let's run it and see the results. The command line running PHPUnit is as follows:
phpunit --bootstrap src/BankAccount.php test/BankAccountTest.php
--bootstrap src/BankAccount.php
Is that the test code to run is loaded before the test code src/BankAccount.php
is run test/BankAccountTest.php
.
If you do not specify a specific test file and only give the directory, PHPUnit will run all files that match the file name in the directory *Test.php
. Because test
there is only BankAccountTest.php
one file in the directory, the execution
phpunit --bootstrap src/BankAccount.php test
will get the same result.
There was 1 failure:1) WarningNo tests found in class "BankAccountTest".FAILURES!Tests: 1, Assertions: 0, Failures: 1.
A warning error because there is no test.
Account instantiation
Let's add a test below. Note that TDD is a design method that helps you to design a module's functionality from the bottom up. When we write the test, we should start from the user's point of view. If the user uses our BankAccount
class, what does he do first? Must be a new instance of a bankaccount. Then our first test is for the instantiated test.
public function testNewAccount(){ $account1 = new BankAccount();}
Run the PHPUnit and fail unexpectedly.
PHP Fatal error: Class 'BankAccount' not found in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 5
There is no BankAccount
definition of the class, we will write the production code below. Make the test pass. src/BankAccount.php
Enter the following in (hereafter referred to as the source file):
Run PHPUnit, test pass.
OK (1 test, 0 assertions)
Next, we will increase the test to make the test fail. If you create a new account, the balance of the account should be 0. So we added a assert
statement:
public function testNewAccount(){ $account1 = new BankAccount(); $this->assertEquals(0, $account1->value());}
Note that value()
is BankAccount
a member function, of course, this function is not yet defined, as a user we would like to BankAccount
provide this function.
Run PHPUnit with the following results:
PHP Fatal error: Call to undefined method BankAccount::value() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 6
The result tells us BankAccount
that there is no value()
such member function. Add Production Code:
class BankAccount { public function value(){ return 0; }}
Why let's value()
return 0 directly because you want to return 0 in the test code value()
. The principle of TDD is not to write redundant production code, just let the test pass.
Access to accounts
After running PHPUnit, we first assume that BankAccount
the instantiation has met the requirements, and then, how do users want to use BankAccount
it? You're going to want to save money in there, uh, want BankAccount
a deposit function, you can increase your account balance by calling this function. So we add the next test.
public function testDeposit(){ $account = new BankAccount(); $account->deposit(10); $this->assertEquals(10, $account->value());}
The initial balance of the account is 0, we deposit 10 yuan, the account balance of course should be 10. Run PHPUnit, the test failed because the deposit function has not yet been defined:
.PHP Fatal error: Call to undefined method BankAccount::deposit() in /home/wuchen/projects/jolly-code-snippets/php/phpunit/test/BankAccountTest.php on line 11
Next, add the deposit function to the source file:
public function deposit($ammount) {}
To run the PHPUnit again, the following results are obtained:
1) BankAccountTest::testDepositFailed asserting that 0 matches expected 10.
Because we do not operate the account balance in the deposit function, the initial value of the balance is still 0 after the 0,deposit function is executed, not the behavior expected by the user. We should increase the value that the user deposited on 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 arbitrarily, so it is defined as a private variable. Here we add a private variable to the production Code $value
, then value
the value that the function should return $value
.
class BankAccount { private $value; public function value(){ return $this->value; } public function deposit($ammount) { $this->value = 10; }}
Run PHPUnit, test pass. Next, we think, what else does the user need? Yes, take the money. This value is deducted from the account balance when the money is taken. If deposit
a negative number is passed to a function, it is equivalent to taking money.
So we add two lines of code to the function that tests the code testDeposit
.
$account->deposit(-5);$this->assertEquals(5, $account->value());
Run the PHPUnit again and the test fails.
1) BankAccountTest::testDepositFailed asserting that 10 matches expected 5.
This is because in the production code we simply put the $value
result set to 10. Improve production code.
public function deposit($ammount) { $this->value += $ammount;}
Run PHPUnit again, test pass.
The new constructor
Next, I thought, the user might need a different constructor, and when BankAccount
the object is created, it can pass in a value as the account balance. So we're testNewAccount
adding this kind of instantiation testing.
public function testNewAccount(){ $account1 = new BankAccount(); $this->assertEquals(0, $account1->value()); $account2 = new BankAccount(10); $this->assertEquals(10, $account2->value());}
Run the PHPUnit with the result:
1) BankAccountTest::testNewAccountFailed asserting that null matches expected 10.
Because BankAccount
there is no constructor with parameters, it new BankAccount(10)
returns an empty object, and the function of the empty object value()
naturally returns NULL. To pass the test, we add constructors with parameters in the production code.
public function __construct($n){ $this->value = $n;}
To run the test again:
1) BankAccountTest::testNewAccountMissing 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:52) BankAccountTest::testDepositMissing 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
Errors are reported in all two calls new BankAccount()
, with the addition of a constructor with parameters, and a constructor with no arguments. c++/java
the students from the transition immediately thought of adding a default constructor:
public function __construct() { $this->value = 0;}
However, this is not possible because PHP does not support function overloading, so you cannot have more than one constructor.
What to do? Yes, we can add a default value for the parameter. Modify the constructor to:
public function __construct($n = 0){ $this->value = $n;}
When this new BankAccount()
is called, it is equivalent to passing 0 to the constructor, which satisfies the requirement.
PHPUnit run the following, test pass.
At this point, our production code is:
value = $n; } public function value(){ return $this->value; } public function deposit($ammount) { $this->value += $ammount; }}?>
Summarize
Although our code is not many, but every step is written with confidence, this is the benefits of TDD. Even if you are not very sure about PHP syntax (like me), you can have confidence in your own code.
Another benefit of writing a program in TDD is that it does not require a careful design of a single module prior to coding, and can be designed when writing tests. This development of the module can meet the needs of users, and will not be redundant.
More usage of PHPUnit will be introduced later.
The above describes the introduction of TDD with PHPUnit, including aspects of the content, I hope that the PHP tutorial interested in a friend helpful.