software testing & phpspec
TRANSCRIPT
“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
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
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
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
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
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
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
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
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
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.
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
CONFIGURATION
# phpspec.ymlsuites: main: namespace: Acme
# composer.json"autoload": { "psr-4": { "Acme\\": "src/Acme" }}
“Users should be able to Register on the system.They need a name, email and password to do so.”
- The Client
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?
LET’S USE PHPSPEC TO HELP
vendor/bin/phpspec describe Acme/User
Specification for Acme\User created in [dir]/spec/UserSpec.php.
namespace spec\Acme;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
class UserSpec extends ObjectBehavior
{
function it_is_initializable()
{
$this->shouldHaveType('Acme\User');
}
}
RUN THE TEST!
$ vendor/bin/phpspec runAcme/User 10 - it is initializable class Acme\User does not exist.
100% 11 specs1 example (1 broken)6ms
Do you want me to create `Acme\User` for you? [Y/n]
- 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.”
USE A CONSTRUCTOR
class UserSpec extends ObjectBehavior{ function let() { $this->beConstructedWith('Darren Craig', '[email protected]', 'abc123'); }
function it_tests_a_users_can_be_registered() { $this->shouldHaveType('Acme\User'); }}
RUN THE TEST
$ vendor/bin/phpspec runAcme/User 15 - it tests a users can be registered method Acme\User::__construct not found.
100% 11 specs1 example (1 broken)9ms
Do you want me to create `Acme\User::__construct()` for you? [Y/n] Y Method Acme\User::__construct() has been created. 100% 11 specs1 example (1 passed)8ms
THE USER CLASS
class User{ public function __construct($name, $email, $password) { // TODO: write logic here }}
RETURNING USER DETAILS
class UserSpec extends ObjectBehavior{ // other tests…
function it_tests_that_it_can_return_a_name() { $this->getName()->shouldReturn('Darren Craig'); }}
RUN THE TEST$ vendor/bin/phpspec runAcme/User 21 - it tests that it can return a name method Acme\User::getName not found.
50% 50% 21 specs2 examples (1 passed, 1 broken)11ms
Do you want me to create `Acme\User::getName()` for you? [Y/n] Y Method Acme\User::getName() has been created. Acme/User 21 - it tests that it can return a name expected "Darren Craig", but got null.
50% 50% 21 specs2 examples (1 passed, 1 failed)12ms
MAKING IT PASS
class User{
private $name;
public function __construct($name, $email, $password) { $this->name = $name; }
public function getName() { return $this->name; }}
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('[email protected]');}
function it_tests_that_it_can_return_the_password(){ $this->getPassword()->shouldReturn('abc123');}
REGISTERING A USER
$input = \Input::all();
$user = new User($input['name'], $input['email'], $input['password']);
$userRepository->save($user);
But, our code should represent the behaviourit’s carrying out…
Are we creating a new User? What are we doing?
- 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.”
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', '[email protected]', 'abc123']);}
THE QUALIFICATION CLASS
$ vendor/bin/phpspec describe Acme/QualificationSpecification for Acme\Qualification created in [dir]/spec/Acme/QualificationSpec.php.
$ vendor/bin/phpspec runAcme/Qualification 10 - it is initializable class Acme\Qualification does not exist.
80% 20% 52 specs5 examples (4 passed, 1 broken)24ms
Do you want me to create `Acme\Qualification` for you? [Y/n] YClass Acme\Qualification created in [dir]/src/Acme/Qualification.php.
100% 52 specs5 examples (5 passed)9ms
MORE USER TESTS…
use Acme\Qualification;
class UserSpec extends ObjectBehavior{ function it_adds_a_qualification(Qualification $qualification) { $this->addQualification($qualification); $this->getQualifications()->shouldHaveCount(1); }}
RUN AND CREATE THE METHODS
$ vendor/bin/phpspec runAcme/User 36 - it adds a qualification method Acme\User::addQualification not found.
80% 20% 52 specs5 examples (4 passed, 1 broken)24ms
Do you want me to create `Acme\User::addQualification()` for you? [Y/n] Y Method Acme\User::addQualification() has been created. Acme/User 31 - it adds a qualification method Acme\User::getQualifications not found.
80% 20% 52 specs5 examples (4 passed, 1 broken)15ms
Do you want me to create `Acme\User::getQualifications()` for you? [Y/n] Y Method Acme\User::getQualifications() has been created. Acme/User 31 - it adds a qualification no haveCount([array:1]) matcher found for null.
80% 20% 52 specs5 examples (4 passed, 1 broken)20ms
AND MAKE IT PASS…
class User
{
private $qualifications = [];
public function addQualification(Qualification $qualification)
{
$this->qualifications[] = $qualification;
}
public function getQualifications()
{
return $this->qualifications;
}
}
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);
}
RUN IT
$ vendor/bin/phpspec runAcme/User 37 - it prevents more than 3 qualifications being added expected to get exception, none got.
83% 16% 62 specs6 examples (5 passed, 1 failed)21ms
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; }}
COMMON MATCHERShttp://phpspec.readthedocs.org/en/latest/cookbook/matchers.html
IDENTITY MATCHERS
$this->getName()->shouldBe("Darren Craig");$this->getName()->shouldBeEqualTo("Darren Craig");$this->getName()->shouldReturn("Darren Craig");$this->getName()->shouldEqual("Darren Craig");
THROW MATCHERS
$this->shouldThrow(\Exception::class)->duringAddQualification($qualification);
$this->shouldThrow(\Exception::class)->during('addQualification', [$qualification]);
TYPE MATCHERS
$this->shouldHaveType('Acme\User');
$this->shouldReturnAnInstanceOf('Acme\User');
$this->shouldBeAnInstanceOf('Acme\User');
$this->shouldImplement('Acme\UserInterface');
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
SCALAR TYPE MATCHER
$this->getName()->shouldBeString();
$this->getQualifications()->shouldBeArray();
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