eloquent/phony

Mocks, stubs, and spies for PHP.

1.0.1 2017-07-04 01:10 UTC

README

Mocks, stubs, and spies for PHP.

Current version image Current build status image Current Windows build status image Tested against HHVM Current coverage status image

Example verification output

Installation

Available as various Composer packages, depending on the test framework in use:

See the section on Integration with test frameworks in the documentation.

Documentation

Full documentation is available.

What is Phony?

Phony is a PHP library for creating test doubles. Phony supports a wide range of PHP versions, from 5.3 through to 7.1, as well as HHVM.

Why use Phony?

Support for language features, old and new

Phony is committed to supporting new PHP features as they emerge. Features that require workarounds in other mocking frameworks (such as passed-by-reference arguments), typically "just work" with Phony.

Amongst other features, Phony supports:

Zero-configuration integrations

Integrating Phony with your favorite test framework is as simple as choosing the correct namespace to import. Complimentary features make integrations seamless and intuitive:

Interested in better integration with other test frameworks? So are we! Just open a GitHub issue if there's something we can do.

Refined verification output

Phony has received a great deal of work and refinement where the rubber meets the road; in its verification output:

Example verification output

Phony's output includes succinct but detailed information that is focused on helping you find the cause of the failure as fast as possible. Where appropriate, the output uses difference comparison and color to further highlight important details.

First-class support for functions and callbacks

Phony is designed to provide support for testing both object-based, and function-based systems. In Phony, object mocks are built upon full-featured function level stubs and spies.

This focus on support for procedural programming allows Phony to handle many situations that cannot be handled by solely class-based mocking frameworks. Since Phony's class-level support is based upon its function-level support, the interfaces are consistent, and require very little extra knowledge to use.

Extensive feature set

Phony has an extensive and powerful feature set. For detailed information on a particular feature, select one of the following:

Help

For help with a difficult testing scenario, questions regarding how to use Phony, or to report issues with Phony itself, please open a GitHub issue so that others may benefit from the outcome.

Alternatively, @ezzatron may be contacted directly via Twitter.

Usage

For detailed usage, see the documentation.

Example test suites

See the phony-examples repository.

Standalone usage

Install the eloquent/phony package, then:

use function Eloquent\Phony\mock;

$handle = mock('ClassA');
$handle->methodA->with('argument')->returns('value');

$mock = $handle->get();

assert($mock->methodA('argument') === 'value');
$handle->methodA->calledWith('argument');

Peridot usage

Install the eloquent/phony-peridot package, then:

use function Eloquent\Phony\mock;

describe('Phony', function () {
    it('integrates with Peridot', function () {
        $handle = mock('ClassA');
        $handle->methodA->with('argument')->returns('value');

        $mock = $handle->get();

        expect($mock->methodA('argument'))->to->equal('value');
        $handle->methodA->calledWith('argument');
    });
});

The eloquent/phony-peridot package also provides auto-wired mocks:

use function Eloquent\Phony\on;

describe('Phony for Peridot', function () {
    it('supports auto-wiring', function (ClassA $mock) {
        $handle = on($mock);
        $handle->methodA->with('argument')->returns('value');

        expect($mock->methodA('argument'))->to->equal('value');
        $handle->methodA->calledWith('argument');
    });
});

Pho usage

Install the eloquent/phony-pho package, then:

use function Eloquent\Phony\Pho\mock;

describe('Phony', function () {
    it('integrates with Pho', function () {
        $handle = mock('ClassA');
        $handle->methodA->with('argument')->returns('value');

        $mock = $handle->get();

        expect($mock->methodA('argument'))->toBe('value');
        $handle->methodA->calledWith('argument');
    });
});

PHPUnit usage

Install the eloquent/phony-phpunit package, then:

use Eloquent\Phony\Phpunit\Phony;

class PhonyTest extends PHPUnit_Framework_TestCase
{
    public function testIntegration()
    {
        $handle = Phony::mock('ClassA');
        $handle->methodA->with('argument')->returns('value');

        $mock = $handle->get();

        $this->assertSame('value', $mock->methodA('argument'));
        $handle->methodA->calledWith('argument');
    }
}

SimpleTest usage

Install the eloquent/phony-simpletest package, then:

use Eloquent\Phony\Simpletest\Phony;

class PhonyTest extends UnitTestCase
{
    public function testIntegration()
    {
        $handle = Phony::mock('ClassA');
        $handle->methodA->with('argument')->returns('value');

        $mock = $handle->get();

        $this->assertSame($mock->methodA('argument'), 'value');
        $handle->methodA->calledWith('argument');
    }
}

Inception

Please forgive me if this section is opinionated, or if I recall some particular detail of a framework incorrectly. But if you want a TL;DR for why I created Phony, basically:

The first mocking framework I used was probably SimpleTest's. Unfortunately, that's a long time ago now, and I didn't really understand mocking at the time. And to top it off, I can't even remember how SimpleTest's mocks functioned. So let's skip ahead to the first mocking framework I really explored in depth, which was PHPUnit's.

When I first discovered PHPUnit's mocks, they were revolutionary to me. They allowed me to test things really thoroughly, in ways I didn't even know were possible previously. Although PHPUnit's mocking was likely a port of some existing Java mocking system, it was the framework that first opened my eyes to the real potential of test doubles.

Unfortunately it wasn't all sunshine and roses. PHPUnit's mocks were difficult to use, and especially, because of the expect-run-verify style interface, they were difficult to re-use. There was no way to "un-expect" something in a particular test, and when something failed, the true cause of the failure was often difficult to determine.

While searching for a better solution, I stumbled across Phake, which was inspired by Java's extremely popular Mockito mocking framework. Phake was, and still is, an excellent mocking framework. Both Mockito and Phake eschew the expect-run-verify pattern, to great benefit.

By treating stubbing and verification as separate concepts, Phake essentially fixed all of the problems I had with PHPUnit's mocks. Mocks could be re-used easily, and when a test failed, the cause was (usually) clear. I was sold on the evils of expect-run-verify, and swore never to go back.

I believe it was around this time that I heard of Mockery. Although I was fairly happy with Phake, it did have a few little oddities, such as the way it deals with by-reference arguments, and mocking of traits was not possible. So I checked out Mockery, but was immediately put off by its use of expectations; which I felt would have been a huge step backwards.

In fairness, it's possible that Mockery supports other mocking methods, but since the "primary" way it works seems to be based around expect-run-verify, I've never considered it a viable candidate, and have never used it.

At some point around this time I worked on a Node.js project, and explored a handful of the most popular Node mocking frameworks, before settling on the excellent Sinon.JS. Its focus on callback-based stubbing and spying, and its extensive verification API would eventually influence Phony heavily.

It wasn't until HHVM became a viable option in the PHP world that I really had to consider using something other than Phake. I had wanted for a while to start experimenting with HHVM in my projects. Unfortunately Phake had issues that prevented the test suite from even running under HHVM, so it was immediately a problem.

One of my co-workers had intended to work on HHVM support for Phake, but unfortunately it seemed at that time as though work on Phake had stagnated, and it took over a month just to get a PR accepted that added HHVM as a test target. To Phake's credit, HHVM is now supported, and hindsight has taught me that HHVM support is hard.

One project that showed promise was Prophecy, an "opinionated" mocking framework that arose from the phpspec testing framework. While I disliked its use of abstract terms like "prophesize" and "reveal" for method names, the core idea of a separate object instance that can be used to control the actual mock worked really well. So well, in fact, that I would eventually end up using this concept in Phony.

Importantly, Prophecy already supported HHVM, and seemed like a great fit to replace Phake; until I ran into the "opinionated" side of Prophecy's nature. One thing that Prophecy does not support is order verification. For example; verifying that your code opens a file before writing to it. It seemed to me to be a feature whose benefits are self-evident, but the Prophecy developers unfortunately did not agree.

So, fool that I am, I thought "How hard can it be?" and started work on Phony, a mocking framework designed to combine the strengths of its predecessors, and allow testing under HHVM without compromise.

New versions of PHP came along and introduced new language features, and Phony adapted to meet the requirements of testing these features. I was also fortunate enough to be part of a development team at my day job, who were willing to be the test bed for Phony, and it received a lot of real-world usage that contributed immensely to Phony's stability and eventual feature set.

Of course it turned into a much longer journey than I first expected, and Phony continues to be a challenging project to maintain. But for me, it's an invaluable tool that I use almost every day, and I hope it can be the same for you.

Thanks for listening.

Erin (@ezzatron)

Thanks

Special thanks to the following people:

License

For the full copyright and license information, please view the LICENSE file.