{"id":15008346,"url":"https://github.com/sirbrillig/spies","last_synced_at":"2025-04-09T16:04:18.324Z","repository":{"id":6194107,"uuid":"54286602","full_name":"sirbrillig/spies","owner":"sirbrillig","description":"Easier spies, stubs, and mocks for PHP testing","archived":false,"fork":false,"pushed_at":"2022-02-10T23:12:52.000Z","size":358,"stargazers_count":25,"open_issues_count":2,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-09T16:04:01.943Z","etag":null,"topics":["mock","mock-functions","php","phpunit","spies","stub","test-spies"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sirbrillig.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-03-19T20:51:08.000Z","updated_at":"2025-03-04T13:13:42.000Z","dependencies_parsed_at":"2022-08-06T19:15:20.902Z","dependency_job_id":null,"html_url":"https://github.com/sirbrillig/spies","commit_stats":null,"previous_names":[],"tags_count":22,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirbrillig%2Fspies","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirbrillig%2Fspies/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirbrillig%2Fspies/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sirbrillig%2Fspies/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sirbrillig","download_url":"https://codeload.github.com/sirbrillig/spies/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248065289,"owners_count":21041871,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["mock","mock-functions","php","phpunit","spies","stub","test-spies"],"created_at":"2024-09-24T19:17:44.169Z","updated_at":"2025-04-09T16:04:18.290Z","avatar_url":"https://github.com/sirbrillig.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Spies\n\nA library to make testing in PHP so much easier. You can install it in a PHP project by following [the instructions below](#installation).\n\nWhat is it? If you've ever used [sinon](http://sinonjs.org/) in JavaScript testing, you know about the concept of *Test Spies*, and in many ways this library is just implementing those concepts in PHP. It also includes *Expectations* to simplify spy assertions, inspired by [sinon-chai](https://github.com/domenic/sinon-chai).\n\nIf you want to just skip to the details, you can [read the API here](API.md).\n\nIf you are not familiar with Test Spies, here's a brief primer: Basically they are objects that behave like functions and keep a record of how they are called. You can inject them into objects when writing tests and monitor the spies to determine if the objects are behaving as you expect.\n\n```php\n$spy = new \\Spies\\Spy();\n$spy( 'hello', 'world' );\n\n$spy-\u003ewas_called(); // Returns true\n$spy-\u003ewas_called_times( 1 ); // Returns true\n$spy-\u003ewas_called_times( 2 ); // Returns false\n$spy-\u003eget_times_called(); // Returns 1\n$spy-\u003ewas_called_with( 'hello', 'world' ); // Returns true\n$spy-\u003ewas_called_with( 'goodbye', 'world' ); // Returns false\n```\n\nSpies can also be programmed to behave in certain ways (in which case they are more properly called \"stubs\" or \"mocks\"), forcing your code down certain paths in order to test specific behavior.\n\n```php\n\\Spies\\stub_function( 'add_one' )-\u003ewhen_called-\u003ewith( 5 )-\u003ewill_return( 6 );\n\\Spies\\stub_function( 'add_one' )-\u003ewhen_called-\u003ewith( 1 )-\u003ewill_return( 2 );\n\nadd_one( 5 ); // Returns 6\nadd_one( 1 ); // Returns 2\n```\n\nIn PHP, we often need to spy on whole objects with instance methods, so Spies provides a mechanism to do that as well:\n\n```php\nclass Greeter {\n    public function say_hello() {\n        return 'hello';\n    }\n\n    public function say_goodbye() {\n        return 'goodbye';\n    }\n}\n\nfunction test_greeter() {\n    $mock = \\Spies\\mock_object_of( 'Greeter' );\n    $mock-\u003eadd_method( 'say_hello' )-\u003ethat_returns( 'greetings' );\n    $greet = $mock-\u003espy_on_method( 'greet' );\n    $this-\u003eassertEquals( 'greetings', $mock-\u003esay_hello() );\n    $this-\u003eassertEquals( null, $mock-\u003esay_goodbye() );\n    $mock-\u003egreet();\n    $this-\u003eassertTrue( $greet-\u003ewas_called() );\n}\n```\n\nThe final piece, Expectations add a layer of syntax to test assertions that should be easier to read as well as providing better failure messages:\n\n```php\nfunction test_spy_is_called_correctly() {\n    $spy = \\Spies\\make_spy();\n    $spy( 'hello', 'world', 7 );\n    $spy( 'hello', 'world', 8 );\n    $expectation = \\Spies\\expect_spy( $spy )-\u003eto_have_been_called-\u003ewith( 'hello', 'world', \\Spies\\any() )-\u003etwice();\n    $expectation-\u003everify();\n}\n```\n\nSpies was designed as an optional replacement for the very excellent [WP_Mock](https://github.com/10up/wp_mock) and [Mockery](http://docs.mockery.io/en/latest/), both of which are powerful but have many aspects and quirks that I don't find intuitive.\n\nSuggestions, bug reports, and feature requests all welcome!\n\n# Installation\n\nA library to make testing in PHP so much easier. You can install it in a PHP project by running:\n\n`composer require --dev sirbrillig/spies`.\n\nThen just make sure you include the autoloader somewhere in your code:\n\nIf using in PHPUnit, you can add autoload to your `phpunit.xml` file:\n```\n\u003cphpunit bootstrap=\"vendor/autoload.php\"\u003e\n...\n```\n\nOtherwise you can include the autoloader manually:\n```\nrequire( './vendor/autoload.php' );\n```\n\nHowever, please see [the note on mocking and spying on existing functions](#spying-and-mocking-existing-functions). If you need to do this, you will need to add an explicit bootstrap file.\n\n# The Details\n\n## Global Functions\n\nIf you  want to create a Spy for a global function (a function in the global namespace, like WordPress's `wp_insert_post`), you can pass the name of the global function to `\\Spies\\get_spy_for()`:\n\n```php\nfunction test_calculation() {\n\t$add_one = \\Spies\\get_spy_for( 'add_together' );\n\n\tadd_together( 2, 3 );\n\n\t$expectation = \\Spies\\expect_spy( $add_one )-\u003eto_have_been_called-\u003ewith( 2, 3 ); // Passes\n\t$expectation-\u003everify();\n}\n```\n\nYou can Spy on functions defined within a namespace in the same way:\n\n```php\nfunction test_calculation() {\n\t$add_one = \\Spies\\get_spy_for( '\\Calculator\\add_together' );\n\n\t\\Calculator\\add_together( 2, 3 );\n\n\t$expectation = \\Spies\\expect_spy( $add_one )-\u003eto_have_been_called-\u003ewith( 2, 3 ); // Passes\n\t$expectation-\u003everify();\n}\n```\n\n## Stubs and Mocks\n\nYou can create stubs with the `\\Spies\\stub_function()` method. A stub is a fake function that can be called like a real function except that you control its behavior.\n\nStubs can also be used to mock a global function or a namespaced function, just like a Spy. In fact, a stub is also a Spy, which means you can query it for any information you like.\n\nThere are a few basic behaviors you can program into a stub:\n\n1. You can simply use one to replace a global function (it will return null).\n2. You can use one to return a specific value when called.\n3. You can use one to return a specific value when called with specific arguments.\n4. You can use one to return one of the arguments it was passed.\n5. You can use one to call a substitute function.\n\nHere's just setting a return value:\n```php\n\\Spies\\stub_function( 'get_color' )-\u003eand_return( 'green' );\n\nget_color(); // Returns 'green'\n```\n\nHere's returning a value with certain arguments:\n```php\n\\Spies\\stub_function( 'add_one' )-\u003ewhen_called-\u003ewith( 5 )-\u003ewill_return( 6 );\n\\Spies\\stub_function( 'add_one' )-\u003ewhen_called-\u003ewith( 1 )-\u003ewill_return( 2 );\n\nadd_one( 5 ); // Returns 6\nadd_one( 1 ); // Returns 2\n```\n\nHere's one returning one of its arguments:\n```php\n\\Spies\\stub_function( 'get_first' )-\u003ewhen_called-\u003ewill_return( \\Spies\\passed_arg( 0 ) );\n\nget_first( 5, 6, 7 ); // Returns 5\nget_first( 1, 2, 3 ); // Returns 1\n```\n\nHere's one returning the result of a substitute function:\n```php\n\\Spies\\stub_function( 'add_one' )-\u003eand_return( function( $a ) {\n\treturn $a + 1;\n} );\n\nadd_one( 5 ); // Returns 6\nadd_one( 1 ); // Returns 2\n```\n\n## Objects\n\nSometimes you need to create a whole object with stubs as functions. In that case you can use `\\Spies\\mock_object()` which will return an object that can be passed around. The object by default has no methods, but you can use `add_method()` to add some.\n\n`add_method()`, or its alias, `spy_on_method()`, when called without a second argument, returns a stub (which, remember, is also a Spy), so you can program its behavior or query it for expectations. You can also use the second argument to pass a function (or Spy) explicitly, in which case whatever you pass is what will be returned.\n\n```php\nfunction test_calculation() {\n\t$adder = \\Spies\\mock_object();\n\t$adder-\u003eadd_method( 'add_one' )-\u003ewhen_called-\u003ewith( 6 )-\u003ewill_return( 7 );\n\t$add_one = $adder-\u003espy_on_method( 'add_one' );\n\n\t$calculator = new Calculator( $adder );\n\t$calculator-\u003eadd_one( 4 ); // Returns null\n\t$calculator-\u003eadd_one( 6 ); // Returns 7\n\n\t\\Spies\\expect_spy( $add_one )-\u003eto_have_been_called(); // Passes\n\t\\Spies\\expect_spy( $add_one )-\u003eto_have_been_called-\u003ewith( 2 ); // Fails\n\t\\Spies\\finish_spying(); // Verifies all Expectations\n}\n```\n\nIt can be tedious to call `add_method()` for every public method of an existing class that you are trying to mock. For that reason you can use `\\Spies\\mock_object_of( $class_name )` which will create a `MockObject` and automatically add a Spy for each public method on the original class. These Spies will all return null by default, but you can replace any of them with your own Spy by using `add_method()` as above.\n\n```php\nclass Greeter {\n\tpublic function say_hello() {\n\t\treturn 'hello';\n\t}\n\n\tpublic function say_goodbye() {\n\t\treturn 'goodbye';\n\t}\n}\n\nfunction test_greeter() {\n\t$mock = \\Spies\\mock_object_of( 'Greeter' );\n\t$mock-\u003eadd_method( 'say_hello' )-\u003ethat_returns( 'greetings' );\n\t$this-\u003eassertEquals( 'greetings', $mock-\u003esay_hello() );\n\t$this-\u003eassertEquals( null, $mock-\u003esay_goodbye() );\n}\n```\n\nIf you'd rather not call `add_method()` and you don't have an original class to copy, you can also just ignore all method calls on the object using `and_ignore_missing()`:\n\n```php\nfunction test_greeter() {\n\t$mock = \\Spies\\mock_object()-\u003eand_ignore_missing();\n\t$this-\u003eassertEquals( null, $mock-\u003esay_goodbye() );\n}\n```\n\n## Object Method Delegation\n\nSometimes it's helpful to be able to be able to spy on actual methods of an object, or to replace some methods on an object, but not others. This involves creating a delegate object, which can be done by passing a class instance to `\\Spies\\mock_object()`.\n\nThe resulting `MockObject` will forward all method calls to the original class instance, except those overridden by using `add_method()`. It's possible to use `spy_on_method()` to spy on any method call of the object, just as you would do with a regular MockObject.\n\n```php\nclass Greeter {\n\tpublic function say_hello() {\n\t\treturn 'hello';\n\t}\n\n\tpublic function say_goodbye() {\n\t\treturn 'goodbye';\n\t}\n}\n\nfunction test_greeter() {\n\t$mock = \\Spies\\mock_object( new Greeter() );\n\t$say_goodbye = $mock-\u003espy_on_method( 'say_goodbye' );\n\t$mock-\u003eadd_method( 'say_hello' )-\u003ethat_returns( 'greetings' );\n\t$this-\u003eassertEquals( 'greetings', $mock-\u003esay_hello() );\n\t$this-\u003eassertEquals( 'goodbye', $mock-\u003esay_goodbye() );\n\t$this-\u003eassertSpyWasCalled( $say_goodbye );\n}\n```\n\n## Expectations\n\nSpies can be useful all by themselves, but Spies also provides the `Expectation` class to make writing your test expectations easier.\n\nLet's say we have a Spy and want to verify if it has been called in a PHPUnit test:\n```php\nfunction test_spy_is_called() {\n\t$spy = \\Spies\\make_spy();\n\t$spy();\n\t$this-\u003eassertTrue( $spy-\u003ewas_called() );\n}\n```\n\nThat works, but here's another way to write it:\n```php\nfunction test_spy_is_called() {\n\t$spy = \\Spies\\make_spy();\n\t$spy();\n\t$expectation = \\Spies\\expect_spy( $spy )-\u003eto_have_been_called();\n\t$expectation-\u003everify();\n}\n```\n\nThey're both totally valid. Expectations just add some more syntactic sugar to your tests and speed the debugging process by improving failure messages. Particularly, they allow building up a set of expected behaviors and then validating all of them at once. Let's use a more complex example. Here it is with just a Spy:\n\n```php\nfunction test_spy_is_called_correctly() {\n\t$spy = \\Spies\\make_spy();\n\t$spy( 'hello', 'world', 7 );\n\t$spy( 'hello', 'world', 8 );\n\t$this-\u003eassertTrue( $spy-\u003ewas_called_with( 'hello', 'world', \\Spies\\any() ) );\n\t$this-\u003eassertTrue( $spy-\u003ewas_called_times( 2 ) );\n}\n```\n\nAnd here with Expectations:\n\n```php\nfunction test_spy_is_called_correctly() {\n\t$spy = \\Spies\\make_spy();\n\t$spy( 'hello', 'world', 7 );\n\t$spy( 'hello', 'world', 8 );\n\t$expectation = \\Spies\\expect_spy( $spy )-\u003eto_have_been_called-\u003ewith( 'hello', 'world', \\Spies\\any() )-\u003etwice();\n\t$expectation-\u003everify();\n}\n```\n\nThat last part, `$expectation-\u003everify()` is what actually tests all the expected behaviors. You can also call the function `\\Spies\\finish_spying()` which will do the same thing, and can be put in a `tearDown` method.\n\n### Better failures\n\nPerhaps the most useful thing about Expectations is that they provide better failure messages. Whereas `$this-\u003eassertTrue( $spy-\u003ewas_called_with( 'hello' ) )` and `\\Spies\\expect_spy( $spy )-\u003eto_have_been_called-\u003ewith( 'hello' )` both assert the same thing, the former will only tell you \"false is not true\", and the Expectation will fail with something like this message:\n\n```\nExpected \"anonymous function\" to be called with ['hello'] but instead it was called with ['goodbye']\n```\n\n### finish_spying\n\nTo complete an expectation during a test, and to keep functions in the global scope from interfering with one another, it's **very important** to call `\\Spies\\finish_spying()` after each test.\n\n`finish_spying()` does three things:\n\n1. Calls `verify()` on each Expectation. `expect_spy()` only prepares the expectation. It is not tested until `verify()` is called.\n2. Clears all current Spies and mocked functions.\n3. Clears all current Expectations.\n\nBecause Expectations are only evaluated when we call `verify()` or `finish_spying()`, you can use expectations before or after the code that is being tested. There's syntactic sugar to make it sound right either way. The following two are the same:\n\n```php\nfunction tearDown() {\n\t\\Spies\\finish_spying();\n}\n\nfunction test_calculation() {\n\t$add_one = \\Spies\\get_spy_for( 'add_together' );\n\n\tadd_together( 2, 3 );\n\n\t\\Spies\\expect_spy( $add_one )-\u003eto_have_been_called-\u003ewith( 2, 3 ); // Passes\n}\n```\n\n```php\nfunction tearDown() {\n\t\\Spies\\finish_spying();\n}\n\nfunction test_calculation() {\n\t$add_one = \\Spies\\get_spy_for( 'add_together' );\n\n\t\\Spies\\expect_spy( $add_one )-\u003eto_be_called-\u003ewith( 2, 3 ); // Passes\n\n\tadd_together( 2, 3 );\n}\n```\n\n### Argument lists\n\nIf you use `with()` to test an Expectation, sometimes you don't care about the value of an argument. In this case you can use `\\Spies\\Expectation::any()` in place of that argument:\n\n```php\nfunction tearDown() {\n\t\\Spies\\finish_spying();\n}\n\nfunction test_calculation() {\n\t$add_one = \\Spies\\get_spy_for( 'add_together' );\n\n\t\\Spies\\expect_spy( $add_one )-\u003eto_be_called-\u003ewith( \\Spies\\Expectation::any(), \\Spies\\Expectation::any() ); // Passes\n\n\tadd_together( 2, 3 );\n}\n```\n\nIf you need to match just part of a string, you can use `\\Spies\\match_pattern()`.\n\n```php\n$spy = \\Spies\\get_spy_for( 'run_experiment' );\nrun_experiment( 'slartibartfast' );\n\\Spies\\expect_spy( $spy )-\u003eto_have_been_called-\u003ewith( \\Spies\\match_pattern( '/bart/' ) );\n\\Spies\\finish_spying();\n```\n\nYou can also use `\\Spies\\match_array()` to match elements of an array while ignoring other parts:\n\n```php\nfunction tearDown() {\n\t\\Spies\\finish_spying();\n}\n\nfunction test_name() {\n\t$say_hello = \\Spies\\get_spy_for( 'say_hello' );\n\n\t\\Spies\\expect_spy( $say_hello )-\u003eto_be_called-\u003ewith( \\Spies\\match_array( [ 'name' =\u003e 'Raistlin' ] ) ) ); // Passes\n\n\tsay_hello( [ 'name' =\u003e 'Raistlin', 'job' =\u003e 'wizard', 'robes' =\u003e 'black' ] );\n}\n```\n\n\n## PHPUnit Custom Assertions\n\nIf you prefer to use PHPUnit custom assertions rather than Expectations, those are also available (although you must base your test class on `\\Spies\\TestCase`):\n\n```php\nclass MyTest extends \\Spies\\TestCase {\n    function test_spy_is_called_correctly() {\n        $spy = \\Spies\\make_spy();\n        $spy( 'hello', 'world', 7 );\n        $spy( 'hello', 'world', 8 );\n        $this-\u003eassertSpyWasCalledWith( $spy, [ 'hello', 'world', \\Spies\\any() ] );\n    }\n}\n```\n\nCustom assertions will provide detailed information about why your test failed, which is much better than \"false is not true\".\n\n```\nFailed asserting that a spy is called with arguments: ( \"a\", \"b\", \"c\" ).\na spy was actually called with:\n 1. arguments: ( \"b\", \"b\", \"c\" ),\n 2. arguments: ( \"m\", \"b\", \"c\" )\n```\n\nSee the [API document](API.md) for the full list of custom assertions available.\n\n## Assertion Helpers\n\nFor any assertion, even those not involving Spies or Stubs, it can be helpful to compare partial arrays in the same manner as `match_array()`. You can use the helper function `do_arrays_match()` to do this:\n\n```php\n$array = [ 'baz' =\u003e 'boo', 'foo' =\u003e 'bar' ];\n$this-\u003eassertTrue( \\Spies\\do_arrays_match( $array, \\Spies\\match_array( [ 'foo' =\u003e 'bar' ] ) ) );\n```\n\n# Spying and Mocking existing functions\n\nPHP does not allow mocking existing functions. However, there is a library called [Patchwork](http://patchwork2.org/) which allows this. If that library is loaded, it will be used by Spies. The library must be loaded *before* Spies. One way to do this is to use a test bootstrap file.\n\nIf using in PHPUnit, you can require the bootstrap from your `phpunit.xml` file:\n```\n\u003cphpunit bootstrap=\"tests/bootstrap.php\"\u003e\n...\n```\n\nHere is an example bootstrap file that also loads the autoloader:\n\n```php\n\u003c?php\n$autoload  = 'vendor/autoload.php';\n$patchwork = 'vendor/antecedent/patchwork/Patchwork.php';\n\n# require patchwork first\nif ( file_exists( $patchwork ) ) {\n\trequire_once $patchwork;\n}\n\nif ( file_exists( $autoload ) ) {\n\trequire_once $autoload;\n}\n```\n\nIf Patchwork is loaded, you will be able to use `mock_function()` and `get_spy_for()` on existing functions:\n\n``` php\nfunction sayHello() {\n  return 'hello';\n}\n//...\n\\Spies\\mock_function( 'sayHello' )-\u003eand_return( 'bye' );\n$this-\u003eassertEquals( 'bye', sayHello() );\n```\n\n``` php\nfunction sayHello() {\n  return 'hello';\n}\n//...\n$spy = \\Spies\\get_spy_for( 'sayHello' );\nsayHello();\n$this-\u003eassertTrue( $spy-\u003ewas_called() );\n```\n\n## Contributing\n\nPlease submit an issue or PR!\n\n[![CircleCI](https://circleci.com/gh/sirbrillig/spies.svg?style=svg)](https://circleci.com/gh/sirbrillig/spies)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsirbrillig%2Fspies","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsirbrillig%2Fspies","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsirbrillig%2Fspies/lists"}