2. “Always code as if the guy who ends up maintaining
your code will be a violent psychopath
and knows where you live.”
- John F. Woods
THE OBLIGATORY QUOTE
4. LEARNING HOW TO TEST
Started looking at unit testing about 2010
Very confusing
So many new concepts, language & ideas
Difficult to implement
Documentation, help and examples were scarce
5.
6. Since then, there are many new tools available
Behat
PHPSpec
Codeception
Documentation has improved, as have help & examples
Frameworks are introducing better standards/practices
BIGGER, BETTER TOOLS
7. GETTING MY HEAD AROUND IT
Started reading about Domain Driven Design
Started using CQRS
Experimented with Datamapper tools, like Doctrine
Started playing with other testing tools, like PHPSpec, Behat & Codeception
And I discovered:
The architecture of my code was a massive issue
I was thinking in terms of the frameworks I was using
“Fat Controllers, Thin Models” - No. (Anaemic Domain Model)
Public attributes on models ($user->name = $blah) weren’t helping
8. THINGS THAT HELPED
I started restructuring my code based on DDD, CQRS and SOLID principles
Made loads of mistakes
… and even more mistakes
Started removing the database structure from my thinking (tough!!!)
Discovered that some mistakes aren’t mistakes
Spoke to a bunch of people on IRC
10. SOFTWARE TESTING
Been around since the late 70s
Checks if a component of a system satisfies the requirements
Usually separated into:
Unit Testing
Integration Testing
Acceptance Testing
11. UNIT TESTING
Tests individual parts - or a unit - of your code
Eg. Does the add() method work properly?
One function/method may have multiple tests
PHPUnit, PHPSpec
12. INTEGRATION TESTING
Tests several parts of your system are working
correctly together
Eg. When a user registers, are their details
saved to the Database?
Behat
13. ACCEPTANCE TESTING
Tests the system is working correctly from a
user’s perspective
E.g. if I go to /register - is the correct form
displayed?
E.g. If I input an invalid email address, do I get
an error?
Behat, Codeception, Selenium
14. TEST DRIVEN DEVELOPMENT (TDD)
Write tests first, then the code that’s being tested
Red-Green-Refactor
Red: Write a test - make it fail
Green: Make the test pass
Refactor: Tidy it up. It should still pass.
16. WHAT IS PHPSPEC?
A PHP Library
Similar to PHPUnit (but with a nicer API!)
Available through Composer (phpspec/phpspec)
Helps design your PHP Classes through
specifications
Describes the behaviour of the class before you
write it
No real difference between SpecBDD and TDD
21. “Users should be able to Register on the system.
They need a name, email and password to do so.”
- The Client
22. LET’S CODE THAT…
$input = Input::all();
$user = new User();
$user->name = $input['name'];
$user->email = $input['email'];
$user->password = Hash::make($input['password']);
$userRepository->save($user);
What’s going on here?
Is this testable?
Is it maintainable?
23. LET’S USE PHPSPEC TO HELP
vendor/bin/phpspec describe Acme/User
Specification for AcmeUser created in [dir]/spec/UserSpec.php.
namespace specAcme;
use PhpSpecObjectBehavior;
use ProphecyArgument;
class UserSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('AcmeUser');
}
}
24. RUN THE TEST!
$ vendor/bin/phpspec run
Acme/User
10 - it is initializable
class AcmeUser does not exist.
100%
1
1 specs
1 example (1 broken)
6ms
Do you want me to create `AcmeUser` for you?
[Y/n]
26. - The Client
WHAT THE CLIENT SAID
“Users should be able to Register on the system.
They need a name, email and password to do so.”
27. USE A CONSTRUCTOR
class UserSpec extends ObjectBehavior
{
function let()
{
$this->beConstructedWith('Darren Craig', 'darren@minus40.co', 'abc123');
}
function it_tests_a_users_can_be_registered()
{
$this->shouldHaveType('AcmeUser');
}
}
28. RUN THE TEST
$ vendor/bin/phpspec run
Acme/User
15 - it tests a users can be registered
method AcmeUser::__construct not found.
100% 1
1 specs
1 example (1 broken)
9ms
Do you want me to create `AcmeUser::__construct()` for you?
[Y/n]
Y
Method AcmeUser::__construct() has been created.
100% 1
1 specs
1 example (1 passed)
8ms
29. THE USER CLASS
class User
{
public function __construct($name, $email, $password)
{
// TODO: write logic here
}
}
30. RETURNING USER DETAILS
class UserSpec extends ObjectBehavior
{
// other tests…
function it_tests_that_it_can_return_a_name()
{
$this->getName()->shouldReturn('Darren Craig');
}
}
31. RUN THE TEST
$ vendor/bin/phpspec run
Acme/User
21 - it tests that it can return a name
method AcmeUser::getName not found.
50% 50% 2
1 specs
2 examples (1 passed, 1 broken)
11ms
Do you want me to create `AcmeUser::getName()` for you?
[Y/n]
Y
Method AcmeUser::getName() has been created.
Acme/User
21 - it tests that it can return a name
expected "Darren Craig", but got null.
50% 50% 2
1 specs
2 examples (1 passed, 1 failed)
12ms
32. MAKING IT PASS
class User
{
private $name;
public function __construct($name, $email, $password)
{
$this->name = $name;
}
public function getName()
{
return $this->name;
}
}
33. RUN THE TEST
$ vendor/bin/phpspec run
100% 2
1 specs
2 examples (2 passed)
7ms
34.
35. THE OTHER USER DETAILS…
function it_tests_that_it_can_return_a_name()
{
$this->getName()->shouldReturn('Darren Craig');
}
function it_tests_that_it_can_return_the_email_address()
{
$this->getEmail()->shouldReturn('darren@minus40.co');
}
function it_tests_that_it_can_return_the_password()
{
$this->getPassword()->shouldReturn('abc123');
}
36. REGISTERING A USER
$input = Input::all();
$user = new User($input['name'], $input['email'], $input['password']);
$userRepository->save($user);
But, our code should represent the behaviour
it’s carrying out…
Are we creating a new User? What are we doing?
37. - The Client
WHAT THE CLIENT SAID
“Users should be able to Register on the system.
They need a name, email and password to do so.”
38. REGISTERING USERS
$input = Input::all();
$user = User::register($input['name'], $input['email'], $input['password']);
$userRepository->save($user);
private function __construct($name, $email, $password) {}
public static function register($name, $email, $password)
{
return new static($name, $email, $password);
}
function let()
{
$this->beConstructedThrough(‘register',
['Darren Craig', 'darren@minus40.co', 'abc123']);
}
41. THE QUALIFICATION CLASS
$ vendor/bin/phpspec describe Acme/Qualification
Specification for AcmeQualification created in [dir]/spec/Acme/QualificationSpec.php.
$ vendor/bin/phpspec run
Acme/Qualification
10 - it is initializable
class AcmeQualification does not exist.
80% 20% 5
2 specs
5 examples (4 passed, 1 broken)
24ms
Do you want me to create `AcmeQualification` for you?
[Y/n]
Y
Class AcmeQualification created in [dir]/src/Acme/Qualification.php.
100% 5
2 specs
5 examples (5 passed)
9ms
42. MORE USER TESTS…
use AcmeQualification;
class UserSpec extends ObjectBehavior
{
function it_adds_a_qualification(Qualification $qualification)
{
$this->addQualification($qualification);
$this->getQualifications()->shouldHaveCount(1);
}
}
43. RUN AND CREATE THE METHODS
$ vendor/bin/phpspec run
Acme/User
36 - it adds a qualification
method AcmeUser::addQualification not found.
80% 20% 5
2 specs
5 examples (4 passed, 1 broken)
24ms
Do you want me to create `AcmeUser::addQualification()` for you?
[Y/n]
Y
Method AcmeUser::addQualification() has been created.
Acme/User
31 - it adds a qualification
method AcmeUser::getQualifications not found.
80% 20% 5
2 specs
5 examples (4 passed, 1 broken)
15ms
Do you want me to create `AcmeUser::getQualifications()` for you?
[Y/n]
Y
Method AcmeUser::getQualifications() has been created.
Acme/User
31 - it adds a qualification
no haveCount([array:1]) matcher found for null.
80% 20% 5
2 specs
5 examples (4 passed, 1 broken)
20ms
44. AND MAKE IT PASS…
class User
{
private $qualifications = [];
public function addQualification(Qualification $qualification)
{
$this->qualifications[] = $qualification;
}
public function getQualifications()
{
return $this->qualifications;
}
}
45. CHECK IF IT PASSED
$ vendor/bin/phpspec run
100% 5
2 specs
5 examples (5 passed)
14ms
47. NO PROBLEM - ANOTHER TEST
function it_prevents_more_than_3_qualifications_being_added(Qualification
$qualification)
{
$this->addQualification($qualification);
$this->addQualification($qualification);
$this->addQualification($qualification);
$this->shouldThrow(Exception::class)->duringAddQualification($qualification);
}
48. RUN IT
$ vendor/bin/phpspec run
Acme/User
37 - it prevents more than 3 qualifications being added
expected to get exception, none got.
83% 16% 6
2 specs
6 examples (5 passed, 1 failed)
21ms
49. AND MAKE IT PASS…
class User
{
private $qualifications = [];
public function addQualification(Qualification $qualification)
{
if(count($this->qualifications) === 3) {
throw new Exception("You can't add more than 3 qualifications");
}
$this->qualifications[] = $qualification;
}
}
56. OBJECT STATE MATCHERS
// calls $user->isOver18();
$this->shouldBeOver18();
// call $user->hasDOB();
$this->shouldHaveDOB();
A nice way of calling is* or has* methods on your object
60. MORE WORK… LESS TEARS
TDD encourages you to think first
Smaller, single-responsibility classes
More maintainable code
More robust systems
As your skill improves, so will your speed
Less likely to spend hours debugging