Pho
BDD test framework for PHP
Install / Use
/learn @danielstjules/PhoREADME

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.
- Installation
- Usage
- Writing Specs
- Running Specs
- Expectations/Matchers
- Custom Matchers
- Reporters
- Mocking
- Namespace
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');
});

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.

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($exp
Related Skills
gh-issues
347.0kFetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]
node-connect
347.0kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
107.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
Writing Hookify Rules
107.8kThis skill should be used when the user asks to "create a hookify rule", "write a hook rule", "configure hookify", "add a hookify rule", or needs guidance on hookify rule syntax and patterns.
