• Stars
    star
    284
  • Rank 142,632 (Top 3 %)
  • Language
    PHP
  • License
    MIT License
  • Created over 10 years ago
  • Updated about 7 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

BDD test framework for PHP

pho

BDD test framework for PHP, inspired by Jasmine and RSpec. Features a familiar syntax, and a watch command to automatically re-run specs during development. It can also be extended with custom matchers and reporters.

Build Status

Installation

The following instructions outline installation using Composer. If you don't have Composer, you can download it from http://getcomposer.org/ If you're new to composer, make sure to add the vendor bin to your PATH:

# Append the following to your profile file, for example in ~/.profile
export PATH=$HOME/.composer/vendor/bin:$PATH

To install pho, run:

composer global require danielstjules/pho

Usage

Usage: pho [options] [files]

Options

   -a   --ascii                     Show ASCII art on completion
   -b   --bootstrap   <bootstrap>   Bootstrap file to load
   -f   --filter      <pattern>     Run specs containing a pattern
   -h   --help                      Output usage information
   -n   --namespace                 Only use namespaced functions
   -r   --reporter    <name>        Specify the reporter to use
   -s   --stop                      Stop on failure
   -v   --version                   Display version number
   -w   --watch                     Watch files for changes and rerun specs
   -C   --no-color                  Disable terminal colors

Writing Specs

Pho exposes a DSL for organizing and writing your tests, which includes the following functions: describe, context, it, before, after, beforeEach, afterEach and expect. Equivalent functions for disabling specs and suites also exist via xdescribe, xcontext and xit.

To create a suite, describe and context can be used by passing them a string and function. Both are interchangeable, though context is more often nested in a describe to group some set of behaviour. it is then used to create a spec, or test.

A spec may contain multiple expectations or assertions, and will pass so long as all assertions pass and no exception is uncaught. For asserting values in pho, expect can be used. The function accepts the value to be tested, and may be chained with a handful of matchers.

<?php

describe('A suite', function() {
    it('contains specs with expectations', function() {
        expect(true)->toBe(true);
    });

    it('can have specs that fail', function() {
        expect(false)->not()->toBe(false);
    });

    it('can have incomplete specs');
});

intro-screenshot

Objects may be passed between suites and specs with php's use keyword. Here's an example:

describe('Example', function() {
    $object = new stdClass();
    $object->name = 'pho';

    context('name', function() use ($object) {
        it('is set to pho', function()  use ($object) {
            expect($object->name)->toBe('pho');
        });
    });
});

Things can get a bit verbose when dealing with multiple objects that need to be passed into closures with use. To avoid such long lists of arguments, $this can be used to set and retrieve values between suites and specs.

describe('SomeClass', function() {
    $this->key1 = 'initialValue';
    $this->key2 = 'initialValue';

    context('methodOne()', function() {
        $this->key1 = 'changedValue';

        it('contains a spec', function() {
            expect($this->key1)->toBe('changedValue');
            expect($this->key2)->toBe('initialValue');
        });
    });

    context('methodTwo()', function() {
        it('contains another spec', function() {
            expect($this->key1)->toBe('initialValue');
            expect($this->key2)->toBe('initialValue');
        });
    });
});

Hooks are available for running functions as setups and teardowns. before is ran prior to any specs in a suite, and after, once all in the suite have been ran. beforeEach and afterEach both run their closures once per spec. Note that beforeEach and afterEach are both stackable, and will apply to specs within nested suites. Furthermore, Global hooks may be defined in your bootstrap file. For example, an afterEach hook in a bootstrap file will run after every test in your suite.

describe('Suite with Hooks', function() {
    $this->count = 0;

    beforeEach(function() {
        $this->count = $this->count + 1;
    });

    it('has a count equal to 1', function() {
        expect($this->count)->toEqual(1);
        // A single beforeEach ran
    });

    context('nested suite', function() {
        beforeEach(function() {
            $this->count = $this->count + 1;
        });

        it('has a count equal to 3', function() {
            expect($this->count)->toEqual(3);
            // Both beforeEach closures incremented the value
        });
    });
});

Running Specs

By default, pho looks for specs in either a test or spec folder under the working directory. It will recurse through all subfolders and run any files ending with Spec.php, ie: userSpec.php. Furthermore, continuous testing is as easy as using the --watch option, which will monitor all files in the path for changes, and rerun specs on save.

watch

Expectations/Matchers

Type Matching

expect('pho')->toBeA('string');
expect(1)->notToBeA('string');
expect(1)->not()->toBeA('string');

expect(1)->toBeAn('integer'); // Alias for toBeA
expect('pho')->notToBeAn('integer');
expect('pho')->not()->toBeA('integer');

Instance Matching

expect(new User())->toBeAnInstanceOf('User');
expect(new User())->not()->toBeAnInstanceOf('Post');
expect(new User())->notToBeAnInstanceOf('Post');

Strict Equality Matching

expect(true)->toBe(true);
expect(true)->not()->toBe(false);
expect(true)->notToBe(false);

expect(['foo'])->toEqual(['foo']); // Alias for toBe
expect(['foo'])->not()->toEqual(true);
expect(['foo'])->notToEqual(true);

Loose Equality Matching

expect(1)->toEql(true);
expect(new User('Bob'))->not()->ToEql(new User('Alice'))
expect(new User('Bob'))->notToEql(new User('Alice'))

Length Matching

expect(['tdd', 'bdd'])->toHaveLength(2);
expect('pho')->not()->toHaveLength(2);
expect('pho')->notToHaveLength(2);

expect([])->toBeEmpty();
expect('pho')->not()->toBeEmpty();
expect('pho')->notToBeEmpty();

Inclusion Matching

expect('Spectacular!')->toContain('Spec');
expect(['a', 'b'])->not()->toContain('c');
expect(['a', 'b'])->notToContain('c');

expect('testing')->toContain('test', 'ing'); // Accepts multiple args
expect(['tdd', 'test'])->not()->toContain('bdd', 'spec');
expect(['tdd', 'test'])->notToContain('bdd', 'spec');

expect(['name' => 'pho'])->toHaveKey('name');
expect(['name' => 'pho'])->not()->toHaveKey('id');
expect(['name' => 'pho'])->notToHaveKey('id');

Pattern Matching

expect('tdd')->toMatch('/\w[D]{2}/i');
expect('pho')->not()->toMatch('/\d+/');
expect('pho')->notToMatch('/\d+/');

expect('username')->toStartWith('user');
expect('spec')->not()->toStartWith('test');
expect('spec')->notToStartWith('test');

expect('username')->toEndWith('name');
expect('spec')->not()->toEndWith('s');
expect('spec')->notToEndtWith('s');

Numeric Matching

expect(2)->toBeGreaterThan(1);
expect(2)->not()->toBeGreaterThan(2);
expect(1)->notToBeGreaterThan(2);

expect(2)->toBeAbove(1); // Alias for toBeGreaterThan
expect(2)->not()->toBeAbove(2);
expect(1)->notToBeAbove(2);

expect(1)->toBeLessThan(2);
expect(1)->not()->toBeLessThan(1);
expect(2)->notToBeLessThan(1);

expect(1)->toBeBelow(2); // Alias for toBeLessThan
expect(1)->not()->toBeBelow(1);
expect(2)->notToBeBelow(1);

expect(1)->toBeWithin(1, 10); // Inclusive
expect(-2)->not()->toBeWithin(-1, 0);
expect(-2)->notToBeWithin(-1, 0);

Print Matching

$callable = function() {
  echo 'test'
};

expect($callable)->toPrint('test');
expect($callable)->not()->toPrint('testing');
expect($callable)->notToPrint('testing');

Exception Matching

$callable = function() {
  throw new Custom\Exception('error!');
};

expect($callable)->toThrow('Custom\Exception');
expect($callable)->not()->toThrow('\ErrorException');
expect($callable)->notToThrow('\ErrorException');

Custom Matchers

Custom matchers can be added by creating a class that implements pho\Expectation\Matcher\MatcherInterface and registering the matcher with pho\Expectation\Expectation::addMatcher(). Below is an example of a basic matcher:

namespace example;

use pho\Expectation\Matcher\MatcherInterface;

class ExampleMatcher implements MatcherInterface
{
    protected $expectedValue;

    public function __construct($expectedValue)
    {
        $this->expectedValue = $expectedValue;
    }

    public function match($actualValue)
    {
        return ($actualValue === $this->expectedValue);
    }

    public function getFailureMessage($negated = false)
    {
        if (!$negated) {
            return "Expected value to be {$this->expectedValue}";
        } else {
            return "Expected value not to be {$this->expectedValue}";
        }
    }
}

Registering it:

use pho\Expectation\Expectation;

// Register the matcher
Expectation::addMatcher('toHaveValue', '\example\ExampleMatcher');

And that's it! You would now have access to the following:

expect($actual)->toHaveValue($expected);
expect($actual)->not()->toHaveValue($expected);
expect($actual)->notToHaveValue($expected);

Reporters

dot (default)

$ pho --reporter dot exampleSpec.php

.FI

Failures:

"A suite can have specs that fail" FAILED
/Users/danielstjules/Desktop/exampleSpec.php:9
Expected false not to be false

Finished in 0.00125 seconds

3 specs, 1 failure, 1 incomplete

spec

$ pho --reporter spec exampleSpec.php

A suite
    contains specs with expectations
    can have specs that fail
    can have incomplete specs

Failures:

"A suite can have specs that fail" FAILED
/Users/danielstjules/Desktop/exampleSpec.php:9
Expected false not to be false

Finished in 0.0012 seconds

3 specs, 1 failure, 1 incomplete

list

$ pho --reporter list exampleSpec.php

A suite contains specs with expectations
A suite can have specs that fail
A suite can have incomplete specs

Failures:

"A suite can have specs that fail" FAILED
/Users/danielstjules/Desktop/exampleSpec.php:9
Expected false not to be false

Finished in 0.0012 seconds

3 specs, 1 failure, 1 incomplete

Mocking

Pho doesn't currently provide mocks/stubs out of the box. Instead, it's suggested that a mocking framework such as prophecy or mockery be used.

Note: Tests can be failed from a test hook. If you need to check mock object expectations after running a spec, you can do so from an afterEach hook.

describe('A suite', function() {
    afterEach(function() {
        Mockery::close();
    });

    it('should check mock object expectations', function() {
        $mock = Mockery::mock('simplemock');
        $mock->shouldReceive('foo')->with(5)->once()->andReturn(10);
        expect($mock->foo(5))->toBe(10);
    });
});

Namespace

If you'd rather not have pho use the global namespace for its functions, you can set the --namespace flag to force it to only use the pho namespace. This will be a nicer alternative in PHP 5.6 with https://wiki.php.net/rfc/use_function

pho\describe('A suite', function() {
    pho\it('contains specs with expectations', function() {
        pho\expect(true)->toBe(true);
    });

    pho\it('can have specs that fail', function() {
        pho\expect(false)->not()->toBe(false);
    });
});

More Repositories

1

jsinspect

Detect copy-pasted and structurally similar code
JavaScript
3,551
star
2

Stringy

A PHP string manipulation library with multibyte support
PHP
2,464
star
3

buddy.js

Magic number detection for JavaScript
JavaScript
867
star
4

pjs

Pipeable javascript. Quickly filter, map, and reduce from the terminal
JavaScript
419
star
5

mocha.parallel

Run async mocha specs in parallel
JavaScript
196
star
6

wsc

WebSocket client for the terminal
JavaScript
167
star
7

blankshield

Prevent reverse tabnabbing phishing attacks caused by _blank
JavaScript
140
star
8

redislock

Node distributed locking using redis
JavaScript
110
star
9

php-pretty-datetime

Generates human-readable strings for PHP DateTime objects
PHP
57
star
10

pattern-emitter

Node event emitters with support for regular expressions
JavaScript
54
star
11

ServerLogStats

A web app that uses javascript and HTML5's FileApi to generate graphs, charts and tables for apache/nginx server logs.
JavaScript
49
star
12

SliceableStringy

Python string slices in PHP
PHP
48
star
13

node-internal-pubsub

A publish/subscribe API similar to that in node_redis, minus the redis
JavaScript
47
star
14

toragent

HTTP(S) requests through Tor for Node
JavaScript
32
star
15

hoops

Nested property access and manipulation lib for node and browser
JavaScript
29
star
16

swaddle

Automagically create API clients/wrappers in JavaScript
JavaScript
23
star
17

async-class

Cleaner ES6 async class methods
JavaScript
22
star
18

defer-analytics

Easily defer loading and firing events for analytics.js
14
star
19

oops.js

JavaScript
5
star
20

filepaths

Get paths to all files in dirs/subdirs in node
JavaScript
4
star
21

battleship-puzzles

Design and implementation of a battleships solitaire puzzle generator and algorithms to solve instances
Python
3
star
22

openports

Find multiple open ports in node
JavaScript
2
star
23

inspect-ast

A better way to view a compact JS abstract syntax tree
JavaScript
2
star
24

freemail-cli

Filters or selects free and disposable email addresses
JavaScript
2
star
25

labtracker

Django app used to keep track of equipment information and use in a university lab setting
Python
2
star
26

sqlmagic

JavaScript
1
star
27

dirmap

Accepts a dir path and returns an object mapping file names to their full path
JavaScript
1
star