software testing & phpspec

62
SOFTWARE TESTING & PHPSPEC DARREN CRAIG @minusdarren

Upload: darren-craig

Post on 22-Jan-2018

490 views

Category:

Software


0 download

TRANSCRIPT

SOFTWARE TESTING & PHPSPEC

DARREN CRAIG@minusdarren

“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

TESTING. TESTING. 123.

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

A QUICK OVERVIEW OF TESTING & TERMINOLOGY

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.

PHPSPEC

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

INSTALLING

"require-dev": { "phpspec/phpspec": "~2.4"},

DID IT WORK?

vendor/bin/phpspec run

0 specs 0 examples 0ms

CONFIGURATION

# phpspec.ymlsuites: main: namespace: Acme

# composer.json"autoload": { "psr-4": { "Acme\\": "src/Acme" }}

DOING AS YOU’RE TOLD…

“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]

YES!

Class Acme\User created in phpspec/src/Acme/User.php.

100% 11 specs1 example (1 passed)13ms

- 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; }}

RUN THE TEST

$ vendor/bin/phpspec run 100% 21 specs2 examples (2 passed)7ms

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']);}

NEXT…

“Users should be able to add up to 3 Qualifications”

- The Client

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;

}

}

CHECK IF IT PASSED

$ vendor/bin/phpspec run 100% 52 specs5 examples (5 passed)14ms

GREAT, BUT…

“Users should be able to add up to 3 Qualifications”

- The Client

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; }}

RUN IT

$ vendor/bin/phpspec run 100% 62 specs6 examples (6 passed)17ms

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");

COMPARISON MATCHER

$this->getAge()->shouldBeLike('21');

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

COUNT MATCHER

$this->getQualifications()->shouldHaveCount(3);

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

QUESTIONS?

THANKS FOR LISTENING!

DARREN CRAIG@minusdarren