diff --git a/API.md b/API.md index d9db280..7be52d7 100644 --- a/API.md +++ b/API.md @@ -1,29 +1,37 @@ # Spies API Reference -# functions +## functions -- `make_spy()`: Shortcut for `new Spy()`. +### `make_spy()` + +Shortcut for `new Spy()`. ```php $spy = make_spy(); $spy(); ``` -- `get_spy_for( $function_name )`: Spy on a global or namespaced function. Shortcut for `Spy::stub_function( $function_name )`. +### `get_spy_for( $function_name )` + +Spy on a global or namespaced function. Shortcut for `Spy::stub_function( $function_name )`. ```php $spy = get_spy_for( 'wp_update_post' ); wp_update_post(); ``` -- `stub_function( $function_name )`: Stub a global or namespaced function. Shortcut for `Spy::stub_function( $function_name )`. +### `stub_function( $function_name )` + +Stub a global or namespaced function. Shortcut for `Spy::stub_function( $function_name )`. ```php stub_function( 'wp_update_post' ); wp_update_post(); ``` -- `mock_function( $function_name )`: Alias for `stub_function()`. +### `mock_function( $function_name )` + +Alias for `stub_function()`. ```php @@ -31,7 +39,9 @@ mock_function( 'wp_update_post' ); wp_update_post(); ``` -- `expect_spy( $spy )`: Shortcut for `Expectation::expect_spy( $spy )`. +### `expect_spy( $spy )` + +Shortcut for `Expectation::expect_spy( $spy )`. ```php @@ -41,7 +51,10 @@ $expectation = expect_spy( $spy )->to_have_been_called(); $expectation->verify(); ``` -- `mock_object()`: Shortcut for `MockObject::mock_object()`. +### `mock_object()` + +Shortcut for `MockObject::mock_object()`. Can also be used to create a +mock object with a delegate. ```php $obj = mock_object(); @@ -49,7 +62,18 @@ $obj->add_method( 'run' ); $obj->run(); ``` -- `mock_object_of( $class_name )`: Mock an instance of an existing class with all its methods. Shortcut for `MockObject::mock_object( $class_name )`. +```php +$mock = \Spies\mock_object( new Greeter() ); +$say_goodbye = $mock->spy_on_method( 'say_goodbye' ); +$mock->add_method( 'say_hello' )->that_returns( 'greetings' ); +$this->assertEquals( 'greetings', $mock->say_hello() ); +$this->assertEquals( 'goodbye', $mock->say_goodbye() ); +$this->assertSpyWasCalled( $say_goodbye ); +``` + +### `mock_object_of( $class_name )` + +Mock an instance of an existing class with all its methods. Shortcut for `MockObject::mock_object( $class_name )`. ```php class TestObj { @@ -60,7 +84,9 @@ $obj = mock_object_of( 'TestObj' ); $obj->run(); ``` -- `finish_spying()`: Resolve all global Expectations, then clear all Expectations and all global Spies. Shortcut for `GlobalExpectations::resolve_delayed_expectations()`, `GlobalExpectations::clear_all_expectations()`, and `GlobalSpies::clear_all_spies`. +### `finish_spying()` + +Resolve all global Expectations, then clear all Expectations and all global Spies. Shortcut for `GlobalExpectations::resolve_delayed_expectations()`, `GlobalExpectations::clear_all_expectations()`, and `GlobalSpies::clear_all_spies`. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -69,7 +95,9 @@ expect_spy( $spy )->to_have_been_called(); finish_spying(); ``` -- `any()`: Used as an argument to `Expectation->with()` to mean "any argument". Shortcut for `new AnyValue()`. +### `any()` + +Used as an argument to `Expectation->with()` to mean "any argument". Shortcut for `new AnyValue()`. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -78,7 +106,9 @@ expect_spy( $spy )->to_have_been_called->with( any() ); finish_spying(); ``` -- `match_pattern()`: Used as an argument to `Expectation->with()` or `Spy()->with()` to mean "any string argument matching this PCRE pattern". Shortcut for `new MatchPattern()`. +### `match_pattern()` + +Used as an argument to `Expectation->with()` or `Spy()->with()` to mean "any string argument matching this PCRE pattern". Shortcut for `new MatchPattern()`. ```php $spy = get_spy_for( 'run_experiment' ); @@ -93,7 +123,9 @@ $id = run_experiment( 'slartibartfast' ); $this->assertEquals( 14, $id ); ``` -- `match_array()`: Used as an argument to `Expectation->with()` or `Spy()->with()` to mean "any argument with these values". Shortcut for `new MatchArray()`. +### `match_array()` + +Used as an argument to `Expectation->with()` or `Spy()->with()` to mean "any argument with these values". Shortcut for `new MatchArray()`. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -108,7 +140,9 @@ $id = wp_update_post( [ 'title' => 'hello', 'status' => 'publish', 'post_content $this->assertEquals( 14, $id ); ``` -- `passed_arg( $index )`: Used as an argument to `Spy->and_return()` to mean "return the passed argument at $index". Shortcut for `new PassedArgument( $index )`. +### `passed_arg( $index )` + +Used as an argument to `Spy->and_return()` to mean "return the passed argument at $index". Shortcut for `new PassedArgument( $index )`. ```php stub_function( 'wp_update_post' )->and_return( passed_arg( 1 ) ); @@ -116,18 +150,22 @@ $value = wp_update_post( 'hello' ); $this->assertEquals( 'hello', $value ); ``` -- `do_arrays_match( $a, $b )`: Compare two arrays allowing usage of `match_array()`. +### `do_arrays_match( $a, $b )` + +Compare two arrays allowing usage of `match_array()`. ```php $array = [ 'baz' => 'boo', 'foo' => 'bar' ]; $this->assertTrue( \Spies\do_arrays_match( $array, \Spies\match_array( [ 'foo' => 'bar' ] ) ) ); ``` -# Spy +## Spy ### Static methods -- `get_spy_for( $function_name )`: Create a new global or namespaced function and attach it to a new Spy, returning that Spy. +### `get_spy_for( $function_name )` + +Create a new global or namespaced function and attach it to a new Spy, returning that Spy. ```php $spy = Spy::get_spy_for( 'wp_update_post' ); @@ -138,7 +176,9 @@ finish_spying(); ### Instance methods -- `get_function_name()`: Return the spy's function name. Really only useful when spying on global or namespaced functions. Defaults to "a spy". +### `get_function_name()` + +Return the spy's function name. Really only useful when spying on global or namespaced functions. Defaults to "a spy". ```php $spy = get_spy_for( 'wp_update_post' ); @@ -147,7 +187,9 @@ $spy2 = make_spy(); $this->assertEquals( 'a spy', $spy2->get_function_name() ); ``` -- `set_function_name()`: Set the spy's function name. You generally don't need to use this. +### `set_function_name()` + +Set the spy's function name. You generally don't need to use this. ```php $spy = make_spy(); @@ -155,7 +197,9 @@ $spy->set_function_name( 'foo' ); $this->assertEquals( 'foo', $spy->get_function_name() ); ``` -- `call( $arg... )`: Call the Spy. It's probably easier to just call the Spy as a function like this: `$spy()`. +### `call( $arg... )` + +Call the Spy. It's probably easier to just call the Spy as a function like this: `$spy()`. ```php $spy = make_spy(); @@ -163,7 +207,9 @@ $spy->call( 1, 2, 3 ); $this->assertSpyWasCalledWith( $spy, [ 1, 2, 3 ] ); ``` -- `call_with_array( $args )`: Call the Spy with an array of arguments. It's probably easier to just call the Spy as a function. +### `call_with_array( $args )` + +Call the Spy with an array of arguments. It's probably easier to just call the Spy as a function. ```php $spy = make_spy(); @@ -171,7 +217,9 @@ $spy->call_with_array( [ 1, 2, 3 ] ); $this->assertSpyWasCalledWith( $spy, [ 1, 2, 3 ] ); ``` -- `clear_call_record()`: Clear the Spy's call record. You shouldn't need to call this. +### `clear_call_record()` + +Clear the Spy's call record. You shouldn't need to call this. ```php $spy = make_spy(); @@ -180,7 +228,9 @@ $spy->clear_call_record(); $this->assertSpyWasNotCalled( $spy ); ``` -- `get_called_functions()`: Get the raw call record for the Spy. Each call is an instance of `SpyCall`. +### `get_called_functions()` + +Get the raw call record for the Spy. Each call is an instance of `SpyCall`. ```php $spy = make_spy(); @@ -189,7 +239,9 @@ $calls = $spy->get_called_functions(); $this->assertEquals( [ 1, 2, 3 ], $calls[0]->get_args() ); ``` -- `was_called()`: Return true if the Spy was called. +### `was_called()` + +Return true if the Spy was called. ```php $spy = make_spy(); @@ -197,7 +249,9 @@ $spy(); $this->assertTrue( $spy->was_called() ); ``` -- `was_called_with( $arg... )`: Return true if the Spy was called with specific arguments. +### `was_called_with( $arg... )` + +Return true if the Spy was called with specific arguments. ```php $spy = make_spy(); @@ -205,7 +259,9 @@ $spy( 'a', 'b' ); $this->assertTrue( $spy->was_called_with( 'a', 'b' ) ); ``` -- `was_called_when( $callable )`: Return true if the passed function returns true at least once. For each spy call, the function will be called with the arguments from that call as an array. +### `was_called_when( $callable )` + +Return true if the passed function returns true at least once. For each spy call, the function will be called with the arguments from that call as an array. ```php $spy = make_spy(); @@ -215,7 +271,9 @@ $this->assertTrue( $spy->was_called_when( function( $args ) { } ) ); ``` -- `was_called_times( $count )`: Return true if the Spy was called exactly `$count` times. +### `was_called_times( $count )` + +Return true if the Spy was called exactly `$count` times. ```php $spy = make_spy(); @@ -224,7 +282,9 @@ $spy(); $this->assertTrue( $spy->was_called_times( 2 ) ); ``` -- `was_called_times_with( $count, $arg... )`: Return true if the Spy was called exactly $count times with specific arguments. +### `was_called_times_with( $count, $arg... )` + +Return true if the Spy was called exactly $count times with specific arguments. ```php $spy = make_spy(); @@ -234,7 +294,9 @@ $spy( 'c', 'd' ); $this->assertTrue( $spy->was_called_times_with( 2, 'a', 'b' ) ); ``` -- `was_called_before( $spy )`: Return true if the Spy was called before $spy. +### `was_called_before( $spy )` + +Return true if the Spy was called before $spy. ```php $spy = make_spy(); @@ -244,7 +306,9 @@ $spy2(); $this->assertTrue( $spy->was_called_before( $spy2 ) ); ``` -- `get_times_called()`: Return the number of times the Spy was called. +### `get_times_called()` + +Return the number of times the Spy was called. ```php $spy = make_spy(); @@ -253,7 +317,9 @@ $spy(); $this->assertEquals( 2, $spy->get_times_called() ); ``` -- `get_call( $index )`: Return the call record for a single call. +### `get_call( $index )` + +Return the call record for a single call. ```php $spy = make_spy(); @@ -263,11 +329,13 @@ $call = $spy->get_call( 0 ); $this->assertEquals( [ 'a' ], $call->get_args() ); ``` -# Stub (Stubs are actually just instances of Spy used differently) +## Stub (Stubs are actually just instances of Spy used differently) ### Static methods -- `stub_function( $function_name )`: Create a new global or namespaced function and attach it to a new Spy, returning that Spy. +### `stub_function( $function_name )` + +Create a new global or namespaced function and attach it to a new Spy, returning that Spy. ```php Spy::stub_function( 'say_hello' )->and_return( 'hello' ); @@ -276,21 +344,27 @@ $this->assertEquals( 'hello', say_hello() ); ### Instance methods -- `and_return( $value )`: Instruct the stub to return $value when called. $value can also be a function to call when the stub is called. +### `and_return( $value )` + +Instruct the stub to return $value when called. $value can also be a function to call when the stub is called. ```php Spy::stub_function( 'say_hello' )->and_return( 'hello' ); $this->assertEquals( 'hello', say_hello() ); ``` -- `will_return( $value )`: Alias for `and_return( $value )`. +### `will_return( $value )` + +Alias for `and_return( $value )`. ```php Spy::stub_function( 'say_hello' )->when_called->will_return( 'hello' ); $this->assertEquals( 'hello', say_hello() ); ``` -- `that_returns( $value )`: Alias for `and_return( $value )`. +### `that_returns( $value )` + +Alias for `and_return( $value )`. ```php $obj = mock_object(); @@ -298,7 +372,9 @@ $obj->add_method( 'run' )->that_returns( 'hello' ); $this->assertEquals( 'hello', $obj->say_hello() ); ``` -- `with( $arg... )`: Changes behavior of next `and_return()` to be a conditional return value. +### `with( $arg... )` + +Changes behavior of next `and_return()` to be a conditional return value. ```php Spy::stub_function( 'say_hello' )->when_called->will_return( 'beep' ); @@ -307,32 +383,40 @@ $this->assertEquals( 'hello', say_hello( 'human' ) ); $this->assertEquals( 'beep', say_hello( 'robot' ) ); ``` -- `when_called`: Syntactic sugar. Returns the Stub. +### `when_called` + +Syntactic sugar. Returns the Stub. ```php Spy::stub_function( 'say_hello' )->when_called->will_return( 'hello' ); $this->assertEquals( 'hello', say_hello() ); ``` -- `and_return_first_argument()`: Shortcut for `and_return( passed_arg( 0 ) )`. +### `and_return_first_argument()` + +Shortcut for `and_return( passed_arg( 0 ) )`. ```php Spy::stub_function( 'say_hello' )->and_return_first_argument(); $this->assertEquals( 'hi', say_hello( 'hi' ) ); ``` -- `and_return_second_argument()`: Shortcut for `and_return( passed_arg( 1 ) )`. +### `and_return_second_argument()` + +Shortcut for `and_return( passed_arg( 1 ) )`. ```php Spy::stub_function( 'say_hello' )->and_return_second_argument(); $this->assertEquals( 'there', say_hello( 'hi', 'there' ) ); ``` -# SpyCall +## SpyCall ## Instance methods -- `get_args()`: Return the arguments for a call. +### `get_args()` + +Return the arguments for a call. ```php $spy = make_spy(); @@ -341,7 +425,9 @@ $calls = $spy->get_called_functions(); $this->assertEquals( [ 1, 2, 3 ], $calls[0]->get_args() ); ``` -- `get_timestamp()`: Return the timestamp for when a call was made. +### `get_timestamp()` + +Return the timestamp for when a call was made. ```php $spy = make_spy(); @@ -351,11 +437,15 @@ $calls = $spy->get_called_functions(); $this->assertGreaterThan( $now, $calls[0]->get_timestamp() ); ``` -# MockObject +## MockObject ### Static methods -- `mock_object()`: Shortcut for `new MockObject()`. +### `mock_object()` + +Shortcut for `new MockObject()`. If a class instance is passed as an +argument, it creates a delegate instance, forwarding all method calls on +the MockObject to the delegate instance. ```php $obj = Spies\MockObject::mock_object(); @@ -363,7 +453,32 @@ $obj->add_method( 'run' ); $obj->run(); ``` -- `mock_object_of( $class_name )`: Create a new `MockObject`, automatically adding a Spy for every public method in `$class_name`. +Using a delegate: + +```php +class Greeter { + public function say_hello() { + return 'hello'; + } + + public function say_goodbye() { + return 'goodbye'; + } +} + +function test_greeter() { + $mock = Spies\MockObject::mock_object( new Greeter() ); + $say_goodbye = $mock->spy_on_method( 'say_goodbye' ); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $this->assertEquals( 'greetings', $mock->say_hello() ); + $this->assertEquals( 'goodbye', $mock->say_goodbye() ); + $this->assertSpyWasCalled( $say_goodbye ); +} +``` + +### `mock_object_of( $class_name )` + +Create a new `MockObject`, automatically adding a Spy for every public method in `$class_name`. ```php class TestObj { @@ -376,7 +491,9 @@ $obj->run(); ### Instance methods -- `add_method( $function_name, $function = null )`: Add a public method to this Object as a Spy and return that method. Creates and returns a Spy if no function is provided. +### `add_method( $function_name, $function = null )` + +Add a public method to this Object as a Spy and return that method. Creates and returns a Spy if no function is provided. ```php $obj = Spies\MockObject::mock_object(); @@ -386,7 +503,9 @@ $obj->add_method( 'run', function( $arg ) { $this->assertEquals( 'hello friend', $obj->run( 'friend' ) ); ``` -- `spy_on_method( $function_name, $function = null )`: Alias for `add_method()`. +### `spy_on_method( $function_name, $function = null )` + +Alias for `add_method()`. ```php $obj = Spies\MockObject::mock_object(); @@ -396,18 +515,22 @@ expect_spy( $spy )->to_have_been_called(); finish_spying(); ``` -- `and_ignore_missing()`: Prevents throwing an Exception when an unmocked method is called on this object. +### `and_ignore_missing()` + +Prevents throwing an Exception when an unmocked method is called on this object. ```php $mock = Spies\mock_object()->and_ignore_missing(); $this->assertEquals( null, $mock->say_goodbye() ); ``` -# Expectation +## Expectation ### Static methods -- `expect_spy( $spy )`: Create a new Expectation for the behavior of $spy. +### `expect_spy( $spy )` + +Create a new Expectation for the behavior of $spy. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -418,7 +541,9 @@ $expectation->verify(); ### Instance methods -- `to_be_called`: Syntactic sugar. Returns the Expectation. +### `to_be_called` + +Syntactic sugar. Returns the Expectation. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -427,7 +552,9 @@ wp_update_post(); $expectation->verify(); ``` -- `to_have_been_called`: Syntactic sugar. Returns the Expectation. +### `to_have_been_called` + +Syntactic sugar. Returns the Expectation. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -436,7 +563,9 @@ $expectation = expect_spy( $spy )->to_have_been_called(); $expectation->verify(); ``` -- `not`: When accessed, reverses all expected behaviors on this Expectation. +### `not` + +When accessed, reverses all expected behaviors on this Expectation. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -445,7 +574,9 @@ $expectation = expect_spy( $spy )->not->to_have_been_called->with( 'hello' ); $expectation->verify(); ``` -- `verify()`: Resolve and verify all the behaviors set on this Expectation. +### `verify()` + +Resolve and verify all the behaviors set on this Expectation. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -454,7 +585,9 @@ $expectation = expect_spy( $spy )->not->to_have_been_called->with( 'hello' ); $expectation->verify(); ``` -- `to_be_called()`: Add an expected behavior that the spy was called when this is resolved. +### `to_be_called()` + +Add an expected behavior that the spy was called when this is resolved. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -463,7 +596,9 @@ wp_update_post(); $expectation->verify(); ``` -- `to_have_been_called()`: Alias for `to_be_called()`. +### `to_have_been_called()` + +Alias for `to_be_called()`. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -472,7 +607,9 @@ $expectation = expect_spy( $spy )->to_have_been_called(); $expectation->verify(); ``` -- `with( $arg... )`: Add an expected behavior that the spy was called with particular arguments when this is resolved. +### `with( $arg... )` + +Add an expected behavior that the spy was called with particular arguments when this is resolved. ```php $spy = get_spy_for( 'wp_update_post' ); @@ -481,7 +618,9 @@ $expectation = expect_spy( $spy )->to_have_been_called->with( 'hello' ); $expectation->verify(); ``` -- `when( $callable )`: Return true if the passed function returns true at least once. For each spy call, the function will be called with the arguments from that call. +### `when( $callable )` + +Return true if the passed function returns true at least once. For each spy call, the function will be called with the arguments from that call. ```php $spy = make_spy(); @@ -492,7 +631,9 @@ expect_spy( $spy )->to_have_been_called->when( function( $args ) { finish_spying(); ``` -- `times( $count )`: Add an expected behavior that the spy was called exactly $count times. +### `times( $count )` + +Add an expected behavior that the spy was called exactly $count times. ```php $spy = make_spy(); @@ -502,7 +643,9 @@ expect_spy( $spy )->to_have_been_called->times( 2 ); finish_spying(); ``` -- `once()`: Alias for `times( 1 )`. +### `once()` + +Alias for `times( 1 )`. ```php $spy = make_spy(); @@ -511,7 +654,9 @@ expect_spy( $spy )->to_have_been_called->once(); finish_spying(); ``` -- `twice()`: Alias for `times( 2 )`. +### `twice()` + +Alias for `times( 2 )`. ```php $spy = make_spy(); @@ -521,7 +666,9 @@ expect_spy( $spy )->to_have_been_called->twice(); finish_spying(); ``` -- `before( $spy )`: Add an expected behavior that the spy was called before $spy. +### `before( $spy )` + +Add an expected behavior that the spy was called before $spy. ```php $spy = make_spy(); @@ -532,21 +679,21 @@ expect_spy( $spy )->to_have_been_called->before( $spy2 ); finish_spying(); ``` -# PHPUnit Custom Assertions +## PHPUnit Custom Assertions These are methods available on instances of `\Spies\TestCase`. ### Constraints for `assertThat()` -- `wasCalled()` -- `wasNotCalled()` -- `wasCalledTimes( $count )` -- `wasCalledBefore( $spy )` -- `wasCalledWhen( $callable )` +### `wasCalled()` +### `wasNotCalled()` +### `wasCalledTimes( $count )` +### `wasCalledBefore( $spy )` +### `wasCalledWhen( $callable )` ### Assertions -- `assertSpyWasCalled( $spy )` +### `assertSpyWasCalled( $spy )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -554,14 +701,14 @@ say_hello(); $this->assertSpyWasCalled( $spy ); ``` -- `assertSpyWasNotCalled( $spy )` +### `assertSpyWasNotCalled( $spy )` ```php $spy = Spy::get_spy_for( 'say_hello' ); $this->assertSpyWasNotCalled( $spy ); ``` -- `assertSpyWasCalledWith( $spy, $args )` +### `assertSpyWasCalledWith( $spy, $args )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -569,7 +716,7 @@ say_hello( 'friend' ); $this->assertSpyWasCalledWith( $spy, [ 'friend' ] ); ``` -- `assertSpyWasNotCalledWith( $spy, $args )` +### `assertSpyWasNotCalledWith( $spy, $args )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -577,7 +724,7 @@ say_hello( 'robot' ); $this->assertSpyWasNotCalledWith( $spy, [ 'friend' ] ); ``` -- `assertSpyWasCalledTimes( $spy, $count )` +### `assertSpyWasCalledTimes( $spy, $count )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -586,7 +733,7 @@ say_hello( 'robot' ); $this->assertSpyWasCalledTimes( $spy, 2 ); ``` -- `assertSpyWasNotCalledTimes( $spy, $count )` +### `assertSpyWasNotCalledTimes( $spy, $count )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -595,7 +742,7 @@ say_hello( 'robot' ); $this->assertSpyWasNotCalledTimes( $spy, 3 ); ``` -- `assertSpyWasCalledTimesWith( $spy, $count, $args )` +### `assertSpyWasCalledTimesWith( $spy, $count, $args )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -604,7 +751,7 @@ say_hello( 'friend' ); $this->assertSpyWasCalledTimesWith( $spy, 2, [ 'friend' ] ); ``` -- `assertSpyWasNotCalledTimesWith( $spy, $count, $args )` +### `assertSpyWasNotCalledTimesWith( $spy, $count, $args )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -613,7 +760,7 @@ say_hello( 'robot' ); $this->assertSpyWasNotCalledTimesWith( $spy, 2, [ 'friend' ] ); ``` -- `assertSpyWasCalledBefore( $spy, $other_spy )` +### `assertSpyWasCalledBefore( $spy, $other_spy )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -623,7 +770,7 @@ say_goodbye(); $this->assertSpyWasCalledBefore( $spy, $other_spy ); ``` -- `assertSpyWasNotCalledBefore( $spy, $other_spy )` +### `assertSpyWasNotCalledBefore( $spy, $other_spy )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -633,7 +780,7 @@ say_hello(); $this->assertSpyWasNotCalledBefore( $spy, $other_spy ); ``` -- `assertSpyWasCalledWhen( $spy, $callable )` +### `assertSpyWasCalledWhen( $spy, $callable )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -643,7 +790,7 @@ $this->assertSpyWasCalledWhen( $spy, function( $args ) { } ); ``` -- `assertSpyWasNotCalledWhen( $spy, $callable )` +### `assertSpyWasNotCalledWhen( $spy, $callable )` ```php $spy = Spy::get_spy_for( 'say_hello' ); diff --git a/README.md b/README.md index 4464e2a..7e68018 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,33 @@ function test_greeter() { } ``` +## Object Method Delegation + +Sometimes 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()`. + +The 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. + +```php +class Greeter { + public function say_hello() { + return 'hello'; + } + + public function say_goodbye() { + return 'goodbye'; + } +} + +function test_greeter() { + $mock = \Spies\mock_object( new Greeter() ); + $say_goodbye = $mock->spy_on_method( 'say_goodbye' ); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $this->assertEquals( 'greetings', $mock->say_hello() ); + $this->assertEquals( 'goodbye', $mock->say_goodbye() ); + $this->assertSpyWasCalled( $say_goodbye ); +} +``` + ## Expectations Spies can be useful all by themselves, but Spies also provides the `Expectation` class to make writing your test expectations easier. diff --git a/src/Spies/Helpers.php b/src/Spies/Helpers.php index 0f90eee..9db4a2d 100644 --- a/src/Spies/Helpers.php +++ b/src/Spies/Helpers.php @@ -49,4 +49,10 @@ private static function do_vals_match( $a, $b ) { public static function do_arrays_match( $a, $b ) { return self::do_vals_match( $a, $b ); } + + public static function make_spy_from_function( $function ) { + $spy = new Spy(); + $spy->will_return( $function ); + return $spy; + } } diff --git a/src/Spies/InvalidFunctionNameException.php b/src/Spies/InvalidFunctionNameException.php new file mode 100644 index 0000000..f4bd691 --- /dev/null +++ b/src/Spies/InvalidFunctionNameException.php @@ -0,0 +1,6 @@ +create_mock_object_for_class( $instance_or_class_name ); + } + $this->create_mock_object_for_delegate( $instance_or_class_name ); + } + + private function create_mock_object_for_delegate( $instance ) { + $this->delegate_instance = $instance; + array_map( [ $this, 'add_method' ], get_class_methods( get_class( $instance ) ) ); + } + + private function create_mock_object_for_class( $class_name ) { $this->class_name = $class_name; - if ( isset( $class_name ) ) { - array_map( [ $this, 'add_method' ], get_class_methods( $class_name ) ); + if ( ! class_exists( $class_name ) ) { + throw new \Exception( 'The class "' . $class_name . '" does not exist and could not be used to create a MockObject' ); } + array_map( [ $this, 'add_method' ], get_class_methods( $class_name ) ); } public function __call( $function_name, $args ) { @@ -19,17 +67,19 @@ public function __call( $function_name, $args ) { if ( $this->ignore_missing_methods ) { return; } - throw new UndefinedFunctionException( 'Attempted to call un-mocked method "' . $function_name . '" with ' . json_encode( $args ) ); + throw new UndefinedFunctionException( 'Attempted to call un-mocked method "' . $function_name . '" with arguments ' . json_encode( $args ) ); } return call_user_func_array( $this->$function_name, $args ); } /** - * Alias for add_method + * Spy on a method on this object + * + * Alias for `add_method()`. * * @param string $function_name The name of the function to add to this object - * @param Spy|callback $function optional A callable function or Spy to be used when the new method is called. Defaults to a new Spy. - * @return Spy|callback The Spy or callback + * @param Spy|function $function optional A callable function or Spy to be used when the new method is called. Defaults to a new Spy. + * @return Spy|function The Spy or callback */ public function spy_on_method( $function_name, $function = null ) { return $this->add_method( $function_name, $function ); @@ -50,19 +100,31 @@ public function spy_on_method( $function_name, $function = null ) { * `$mock_object->add_method( 'do_something' )->that_returns( 'hello world' );` * * @param string $function_name The name of the function to add to this object - * @param Spy|callback $function optional A callable function or Spy to be used when the new method is called. Defaults to a new Spy. - * @return Spy|callback The Spy or callback + * @param Spy|function $function optional A callable function or Spy to be used when the new method is called. Defaults to a new Spy. + * @return Spy|function The Spy or callback */ public function add_method( $function_name, $function = null ) { if ( ! isset( $function ) ) { - $function = isset( $this->$function_name ) ? $this->$function_name : new \Spies\Spy(); + $function = isset( $this->$function_name ) ? $this->$function_name : new Spy(); + } + $reserved_method_names = [ + 'add_method', + 'spy_on_method', + 'and_ignore_missing', + ]; + if ( in_array( $function_name, $reserved_method_names ) ) { + throw new \Spies\InvalidFunctionNameException( 'The function "' . $function_name . '" added to this mock object could not be used because it conflicts with a built-in function' ); } if ( ! is_callable( $function ) ) { - throw new \Exception( 'The function "' . $function_name . '" added to this mock object was not a function' ); + throw new \InvalidArgumentException( 'The function "' . $function_name . '" added to this mock object was not a function' ); + } + if ( $function instanceof Spy && $this->delegate_instance ) { + $function->will_return( [ $this->delegate_instance, $function_name ] ); } - if ( $function instanceof Spy ) { - $function->set_function_name( $function_name ); + if ( ! $function instanceof Spy ) { + $function = Helpers::make_spy_from_function( $function ); } + $function->set_function_name( $function_name ); $this->$function_name = $function; return $function; } @@ -77,11 +139,11 @@ public function and_ignore_missing() { return $this; } - public static function mock_object( $class_name = null) { + public static function mock_object( $class_name = null ) { return new MockObject( $class_name ); } - public static function mock_object_of( $class_name = null) { + public static function mock_object_of( $class_name = null ) { return new MockObject( $class_name ); } } diff --git a/src/Spies/functions.php b/src/Spies/functions.php index 8f72605..938d6a9 100644 --- a/src/Spies/functions.php +++ b/src/Spies/functions.php @@ -31,8 +31,8 @@ function finish_spying() { \Spies\GlobalSpies::restore_original_global_functions(); } -function mock_object() { - return \Spies\MockObject::mock_object(); +function mock_object( $instance = null ) { + return \Spies\MockObject::mock_object( $instance ); } function mock_object_of( $class_name = null ) { diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index fe6602b..0693eb1 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -8,6 +8,10 @@ public function say_hello() { public function say_goodbye() { return 'goodbye'; } + + public function just_say( $what ) { + return 'yo' . $what; + } } /** @@ -62,13 +66,141 @@ public function test_mock_object_of_allow_overriding_methods() { $this->assertEquals( null, $mock->say_goodbye() ); } - public function test_spy_on_method_is_an_alias_for_add_method() { + public function test_spy_on_method_for_non_existent_method_is_an_alias_for_add_method() { $mock = \Spies\mock_object_of( 'Greeter' ); $mock->spy_on_method( 'say_hello' )->that_returns( 'greetings' ); $this->assertEquals( 'greetings', $mock->say_hello() ); $this->assertEquals( null, $mock->say_goodbye() ); } + public function test_spy_on_method_for_existing_method_stub_does_not_break_method() { + $mock = \Spies\mock_object(); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $mock->spy_on_method( 'say_hello' ); + $this->assertEquals( 'greetings', $mock->say_hello() ); + } + + public function test_spy_on_method_for_existing_method_stub_returns_the_stub() { + $mock = \Spies\mock_object(); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $mock->spy_on_method( 'say_hello' )->that_returns( 'foobar' ); + $this->assertEquals( 'foobar', $mock->say_hello() ); + } + + public function test_mock_object_with_instance_delegates_methods_to_instance_methods() { + $mock = \Spies\mock_object( new Greeter() ); + $this->assertEquals( 'hello', $mock->say_hello() ); + } + + public function test_mock_object_with_instance_delegates_methods_to_instance_methods_with_arguments() { + $mock = \Spies\mock_object( new Greeter() ); + $this->assertEquals( 'yono', $mock->just_say( 'no' ) ); + } + + public function test_add_method_on_a_delegate_instance_overrides_the_instance_method() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $this->assertEquals( 'greetings', $mock->say_hello() ); + } + + public function test_add_method_with_a_function_on_a_delegate_instance_sends_the_arguments_to_the_function() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'just_say', function( $what ) { + return 'just ' . $what; + } ); + $this->assertEquals( 'just thanks', $mock->just_say( 'thanks' ) ); + } + + public function test_add_method_on_a_delegate_instance_overrides_the_instance_method_only_if_conditions_are_met() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'just_say' )->when_called->with( 'no' )->will_return( 'nope' ); + $this->assertEquals( 'yoyes', $mock->just_say( 'yes' ) ); + } + + public function test_add_method_on_a_delegate_instance_overrides_the_instance_method_and_receives_its_arguments() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'just_say' )->when_called->with( 'no' )->will_return( 'nope' ); + $this->assertEquals( 'nope', $mock->just_say( 'no' ) ); + } + + public function test_add_method_with_return_function_on_a_delegate_instance_overrides_the_instance_method_and_receives_its_arguments() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'just_say' )->when_called->with( 'cool' )->will_return( function( $arg ) { + return $arg . ' is cool'; + } ); + $this->assertEquals( 'cool is cool', $mock->just_say( 'cool' ) ); + } + + public function test_add_method_with_a_function_on_a_class_mock_sends_the_arguments_to_the_function() { + $mock = \Spies\mock_object( 'Greeter' ); + $mock->add_method( 'just_say', function( $what ) { + return 'just ' . $what; + } ); + $this->assertEquals( 'just thanks', $mock->just_say( 'thanks' ) ); + } + + public function test_spy_on_method_for_a_class_which_was_overridden_with_a_function_returns_spy_which_is_triggered_by_method() { + $mock = \Spies\mock_object( 'Greeter' ); + $mock->add_method( 'just_say', function( $what ) { + return 'saying ' . $what; + } ); + $spy = $mock->spy_on_method( 'just_say' ); + $mock->just_say( 'hi' ); + $this->assertTrue( $spy->was_called_with( 'hi' ) ); + } + + public function test_spy_on_method_for_a_delegate_instance_which_was_overridden_with_a_function_returns_spy_which_is_triggered_by_method() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'just_say', function( $what ) { + return 'saying ' . $what; + } ); + $spy = $mock->spy_on_method( 'just_say' ); + $mock->just_say( 'hi' ); + $this->assertTrue( $spy->was_called_with( 'hi' ) ); + } + + public function test_spy_on_method_for_a_delegate_instance_which_was_overridden_returns_spy_which_is_triggered_by_method() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $spy = $mock->spy_on_method( 'say_hello' ); + $mock->say_hello(); + $this->assertTrue( $spy->was_called() ); + } + + public function test_spy_on_method_for_a_delegate_instance_does_not_break_the_instance_method() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->spy_on_method( 'say_hello' ); + $this->assertEquals( 'hello', $mock->say_hello() ); + } + + public function test_spy_on_method_for_a_delegate_instance_returns_stub_which_can_override_the_instance_method() { + $mock = \Spies\mock_object( new Greeter() ); + $mock->spy_on_method( 'say_hello' )->will_return( 'foobar' ); + $this->assertEquals( 'foobar', $mock->say_hello() ); + } + + public function test_spy_on_method_for_a_delegate_instance_returns_spy_which_is_triggered_by_existing_method() { + $mock = \Spies\mock_object( new Greeter() ); + $spy = $mock->spy_on_method( 'say_hello' ); + $mock->say_hello(); + $this->assertTrue( $spy->was_called() ); + } + + public function test_spy_on_method_for_existing_method_stub_returns_spy_which_is_triggered_by_existing_method() { + $mock = \Spies\mock_object(); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $spy = $mock->spy_on_method( 'say_hello' ); + $mock->say_hello(); + $this->assertTrue( $spy->was_called() ); + } + + public function test_spy_on_method_for_mock_object_method_returns_spy_which_is_triggered_by_existing_method() { + $mock = \Spies\mock_object_of( 'Greeter' ); + $spy = $mock->spy_on_method( 'say_hello' ); + $mock->say_hello(); + $this->assertTrue( $spy->was_called() ); + } + public function test_mock_object_throws_error_when_unmocked_method_is_called() { $this->setExpectedException( '\Spies\UndefinedFunctionException' ); $mock = \Spies\mock_object(); @@ -95,4 +227,28 @@ public function test_mock_object_with_two_calls_to_add_method_allows_a_default() $this->assertEquals( 5, $mock->test_stub( 'hello' ) ); $this->assertEquals( 6, $mock->test_stub( 'bar' ) ); } + + public function test_mock_object_throws_error_when_mocking_reserved_method_name_add_method() { + $this->setExpectedException( '\Spies\InvalidFunctionNameException' ); + $mock = \Spies\mock_object(); + $mock->add_method( 'add_method' ); + } + + public function test_mock_object_throws_error_when_mocking_reserved_method_name_spy_on_method() { + $this->setExpectedException( '\Spies\InvalidFunctionNameException' ); + $mock = \Spies\mock_object(); + $mock->add_method( 'spy_on_method' ); + } + + public function test_mock_object_throws_error_when_mocking_reserved_method_name_and_ignore_missing() { + $this->setExpectedException( '\Spies\InvalidFunctionNameException' ); + $mock = \Spies\mock_object(); + $mock->add_method( 'and_ignore_missing' ); + } + + public function test_mock_object_throws_error_when_mocking_with_a_non_function() { + $this->setExpectedException( 'InvalidArgumentException' ); + $mock = \Spies\mock_object(); + $mock->add_method( 'foobar', 42 ); + } }