Fork me on GitHub

Sham - test stub and spy for PHP

Sham is test stub and spy (test double) for PHP 5.3 and up. Sham records every interaction you have with it, you can then later investigate what happened. It does not self-verify, use your testing framework for asserting.

Download and Install

Download the latest version here.

Manual

Introduction

Sham is a mocking library, but to use a more correct term, it's a test stub and spy. It uses the record-then-assert paradigm, which is more suitable for Behavior Driven Development, where the tests themselves only assert. Sham does not self-verify, you have to make the assertions yourself using your test framework (there are plans to make it assert).

There is no need to set expectations beforehand, just inject a Sham in place of a real object and let it record.

Stubbing

You can create a stub by instantiating the sham\Stub class directly:

$stub = new \sham\Stub();

However, if the object you are trying to stub must be an instance of a certain class, use the static method Sham::create():

$stub = Sham::create('My\Class');

$stub is now an instance of My\Class and will pass any instanceof or typhint checks. What the create method does is, it takes the source of the sham\Stub class as string, augments it to be an instance of My\Class and eval()s that code. It implements all the neccessary methods to adhere to any abstract classes in the class hierarchy.

You can also stub interfaces. Just pass in a name of an interface. You get back an object which implements that interface.

Return values

Every call returns a \sham\Stub instance by default. You can, however, set a return value:

$stub->method->returns('foo');
$stub->method(); // 'foo'

Calls will keep returning the same value. Any subsequent calls to method() will now return 'foo'.

Stubbing by parameters

You can also stub a method to return a certain value given specific parameters.

$stub->method->given('foo')->returns('bar');
$stub->method('foo'); // 'bar'

// fallback to default return value when params don't match
$stub->method(); // \sham\Stub

You can call given() multiple times. These will be added to a stack where the top most calls get priority:

$stub->method->given('zero', Sham::any())->returns('first');
$stub->method->given(Sham::any(), 0)->returns('second');

$stub->method('zero', 3); // 'first'
$stub->method('one', 0);  // 'second'
$stub->method('zero', 0); // 'second' (later stubs get priority)

The given()->... pattern also applies to exceptions and side effects. That is, you can replace the returns() call with throws() or does(). More on these next.

Throwing exceptions

To make a call throw an exception on invokation, use the throws() method. The first parameter tells it which exception to throw. You can give it a name of an exception as a string, or an instance of an exception class. If not given anything, it will throw a sham\Exception. All of the examples below will set method() to throw a sham\Exception when invoked:

$stub->method->throws('sham\Exception');
$stub->method->throws(new \sham\Exception());
$stub->method->throws();

Side effects

You can also make methods run code when they are invoked. These side effects can be added with the does() method. It accepts an anonymous function as it's only param.

$stub->method->does(function () {
    bar();
    return 'foo';
});

$stub->method(); // calls bar and returns 'foo'

Think hard before you use this. It's very likely that you need to refactor your code before ever needing to use this.

Recording

Method calls

Sham does not distinguish between normal and overloaded method calls. You filter both with just the called method's name. For example, if you call a method overload(), which would be an overloaded method in the real implementation:

$stub->overload();

$stub->got('overload')->once(); // true
$stub->got('__call')->never();  // true, no __call call is ever recorded

__invoke

A sham\Stub instance can be invoked. A call with name __invoke is recorded:

$stub();
$stub->got('__invoke')->once(); // true

$stub('foo');
$stub->got('__invoke', 'foo')->once(); // true

Return values for __invoke calls can be set just like for method calls. Either:

$stub->__invoke->returns('return value');
$stub(); // 'return value'

or using a convenience method returns() on the stub itself:

$stub->returns('return value');
$stub(); // 'return value'

Serializing

Stubs can be serialized and unserialized. Sham records both __sleep and __wakeup.

$stub = new \sham\Stub();
$waken = unserialize(serialize($stub));

$waken->got('__sleep')->once(); // true
$waken->got('__wakeup')->once(); // true

Stubbed method return values and exceptions are preserved but side effects are not. This is because they are implemented with anonymous functions and PHP can't serialize those.

$stub->method->given('something')->returns('return value');
$waken = unserialize(serialize($stub));

$waken->method('something'); // 'return value'

Filtering calls (asserting)

To investigate your stub objects you use the got() method. It filters the calls by given criteria and returns a call list object (sham\CallList). The call list object has some helpful methods you can use when asserting. These methods don't throw exceptions. Use your test runner for actual asserting.

To check if foo() was called on $stub you would do this:

$stub = new \sham\Stub();
$stub->foo();

$stub->got('foo')->once(); // true

To check if foo() was called once with 'first' as the only parameter:

$stub->got('foo', 'first')->once();

To check if foo() was called with anything as the first param and bar as the second param:

$stub->got('foo', Sham::any(), 'bar')->once();

The special Sham::any() call returns a matcher object which matches anything. This is useful when you are writing a test which only needs to test a certain parameter and ignore the others.

Data objects

Sham objects can act as value or entity objects. All property access, array access and iteration is recorded. The data it operates on is set using shamSetData() or by setting the properties and array indexes directly. Property access, array access and iteration all operate on the same data.

$stub->shamSetData(array(
    'key' => 'value',
));

$stub->key   // 'value'
$stub['key'] // 'value'

Properties

Properties can be set and retrieved, and it works just like you'd expect. Under the hood all the calls get recorded. This is useful when you are stubbing out an entity or an Active Record object:

$record = new \sham\Stub();

$record->name = 'Antti';
$record->save();

$record->got('__set', 'name', 'Antti')->once(); // true
$record->got('save')->once();                   // true

// ditto.
$record->name // 'Antti'
$stub->got('__get', 'name')->once(); // true

If you call isset() on a non-existent property, and __isset() call will be recorded.

isset($stub->invalid); // false
$stub->got('__isset', 'invalid')->once(); // true

If you unset a property, the property will be unset and a __unset call will be recorded.

$stub->prop = 'value';
unset($stub->prop);
$stub->got('__unset', 'prop')->once(); // true

ArrayAccess

Sham implements the ArrayAccess interface and records all of those calls.

// retrieve with array access
$stub['key'] // 'value'
$stub->got('offsetGet', 'key')->once(); // true

// set offset
$stub['other'] = 'value';
$stub->got('offsetSet', 'other', 'value')->once(); // true

Iteration

You can iterate over the data you've set with \sham\Stub::shamSetData(). All of the calls implemented by Iterator will be recorded.

API

Sham

Methods:

sham\Stub:

Methods:

sham\CallList:

Methods:

sham\Call:

Properties:

License

Sham is licensed under the BSD license.

Copyright (c) 2010, Antti Holvikari
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.