From 8f4f8659ecc5fbdaa66e04f0f20899968c8a5a0a Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 13 Oct 2017 18:15:47 -0400 Subject: [PATCH 01/21] Tests: add tests for spying on MockObject delegates --- tests/MockObjectTest.php | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index fe6602b..4ff28fe 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -62,13 +62,51 @@ 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_override_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_mock_object_with_instance_delegates_methods_to_instance_methods() { + $mock = \Spies\mock_object( new Greeter() ); + $this->assertEquals( 'hello', $mock->say_hello() ); + } + + public function test_add_method_on_a_delegate_instance_overrides_the_instance_method() { + $mock = \Spies\mock_object( new Greeter() ); + $this->assertEquals( 'hello', $mock->say_hello() ); + } + + public function test_spy_on_method_for_a_delegate_instance_does_not_override_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_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_existing_real_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(); From 157f74038b7d4e0a51a940c0b212b400a9f5f34d Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Fri, 13 Oct 2017 18:17:09 -0400 Subject: [PATCH 02/21] Allow MockObject spies to trigger on delegate methods --- src/Spies/MockObject.php | 54 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index c161f99..b3718d3 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -3,13 +3,42 @@ class MockObject { + private $spies_on_methods = []; private $class_name = null; - private $ignore_missing_methods = false; + /** + * Create a new MockObject + * + * If passed nothing, the MockObject will have no methods. Methods can be + * added by using the `add_method()` method (hopefully you don't need to add + * a method called `add_method` or you're out of luck). That method allows + * passing a function to use as the method or it returns a Stub (which is a + * Spy) that can be used to program the method using the usual Stub API. + * + * If passed a class name, the MockObject will automatically have Stub + * methods added for each method of the class you specified. These methods + * will, by default, do nothing. It is still possible, then, to use + * `add_method()` to add new methods or to modify the behavior of existing + * methods. + * + * Normally, if a method is called on the MockObject which has not been + * defined by `add_method()`, an Exception will be thrown. However, if you + * want to allow and ignore any un-mocked methods, you can call + * `and_ignore_missing()` on the MockObject. + * + * In all of these cases, the methods of the MockObject can be used as Spies + * to determine their usage. To spy on a method in this class, call + * `spy_on_method()`. + * + * @param string $class_name optional. The class to mock. + */ public function __construct( $class_name = null ) { $this->class_name = $class_name; if ( isset( $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 ) ); } } @@ -25,13 +54,20 @@ public function __call( $function_name, $args ) { } /** - * Alias for add_method + * Spy on a method on this object + * + * If the method does not already exist, this is an alias for `add_method()`. + * If the method _does_ exist, this will install a Spy on the existing method + * and return that Spy. * * @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 */ public function spy_on_method( $function_name, $function = null ) { + if ( isset( $this->spies_on_methods[ $function_name ] ) ) { + return $this->spies_on_methods[ $function_name ]; + } return $this->add_method( $function_name, $function ); } @@ -57,13 +93,23 @@ public function add_method( $function_name, $function = null ) { if ( ! isset( $function ) ) { $function = isset( $this->$function_name ) ? $this->$function_name : new \Spies\Spy(); } + $reserved_method_names = [ + 'add_method', + 'spy_on_method', + 'and_ignore_missing', + ]; + if ( in_array( $function_name, $reserved_method_names ) ) { + throw new \Exception( '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' ); } if ( $function instanceof Spy ) { $function->set_function_name( $function_name ); } + $function_spy = ( $function instanceof Spy ) ? $function : new \Spies\Spy(); $this->$function_name = $function; + $this->spies_on_methods[ $function_name ] = $function_spy; return $function; } @@ -77,11 +123,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 ); } } From 36148fb1039d0efa99a708b84e1c507ae15581c4 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Sat, 14 Oct 2017 14:13:57 -0400 Subject: [PATCH 03/21] Tests: Improve delegate instance tests --- tests/MockObjectTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index 4ff28fe..191a1bf 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -83,7 +83,8 @@ public function test_mock_object_with_instance_delegates_methods_to_instance_met public function test_add_method_on_a_delegate_instance_overrides_the_instance_method() { $mock = \Spies\mock_object( new Greeter() ); - $this->assertEquals( 'hello', $mock->say_hello() ); + $mock->add_method( 'say_hello' )->that_returns( 'greetings' ); + $this->assertEquals( 'greetings', $mock->say_hello() ); } public function test_spy_on_method_for_a_delegate_instance_does_not_override_the_instance_method() { @@ -92,6 +93,13 @@ public function test_spy_on_method_for_a_delegate_instance_does_not_override_the $this->assertEquals( 'hello', $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' ); From 197d835e8a19cd8375a37f0b81cb93eb557980f1 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Sat, 14 Oct 2017 14:21:49 -0400 Subject: [PATCH 04/21] Allow MockObject to accept delegate instance --- src/Spies/MockObject.php | 30 +++++++++++++++++++++++++----- src/Spies/functions.php | 4 ++-- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index b3718d3..c02f771 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -5,6 +5,7 @@ class MockObject { private $spies_on_methods = []; private $class_name = null; + private $delegate_instance = null; private $ignore_missing_methods = false; /** @@ -22,6 +23,12 @@ class MockObject { * `add_method()` to add new methods or to modify the behavior of existing * methods. * + * If passed a class instance, the MockObject will act as a proxy, delegating + * all method calls to the class instance it was passed. However, it will + * still retain the features of a MockObject, like being able to add + * additional methods (using `add_method()`) and being able to spy on method + * calls (using `spy_on_method()`). + * * Normally, if a method is called on the MockObject which has not been * defined by `add_method()`, an Exception will be thrown. However, if you * want to allow and ignore any un-mocked methods, you can call @@ -31,16 +38,24 @@ class MockObject { * to determine their usage. To spy on a method in this class, call * `spy_on_method()`. * - * @param string $class_name optional. The class to mock. + * @param string $instance_or_class_name optional. An instance of a class or a class name to mock. */ - public function __construct( $class_name = null ) { - $this->class_name = $class_name; - if ( isset( $class_name ) ) { + public function __construct( $instance_or_class_name = null ) { + if ( ! isset( $instance_or_class_name ) ) { + return; + } + if ( is_string( $instance_or_class_name ) ) { + $class_name = $instance_or_class_name; + $this->class_name = $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 ) ); + return; } + $instance = $instance_or_class_name; + $this->delegate_instance = $instance; + array_map( [ $this, 'add_method' ], get_class_methods( get_class( $instance ) ) ); } public function __call( $function_name, $args ) { @@ -48,7 +63,7 @@ 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 ); } @@ -106,6 +121,11 @@ public function add_method( $function_name, $function = null ) { } if ( $function instanceof Spy ) { $function->set_function_name( $function_name ); + if ( $this->delegate_instance ) { + $function->will_return( function() use ( $function_name ) { + return call_user_func_array( [ $this->delegate_instance, $function_name ], func_get_args() ); + } ); + } } $function_spy = ( $function instanceof Spy ) ? $function : new \Spies\Spy(); $this->$function_name = $function; 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 ) { From 6c589fbe4b06445f497f3196c4a183d5d1327197 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Sat, 14 Oct 2017 14:34:08 -0400 Subject: [PATCH 05/21] Tests: add additional verification of overridden delegating --- tests/MockObjectTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index 191a1bf..263b7ca 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -87,6 +87,14 @@ public function test_add_method_on_a_delegate_instance_overrides_the_instance_me $this->assertEquals( 'greetings', $mock->say_hello() ); } + 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_override_the_instance_method() { $mock = \Spies\mock_object( new Greeter() ); $mock->spy_on_method( 'say_hello' ); From d2cae4aaa61c017a65496a8ad50c4d9eb6e8d115 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 18:31:54 -0400 Subject: [PATCH 06/21] Tests: Add more tests around spy_on_method --- tests/MockObjectTest.php | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index 263b7ca..b6d7dde 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -69,13 +69,20 @@ public function test_spy_on_method_for_non_existent_method_is_an_alias_for_add_m $this->assertEquals( null, $mock->say_goodbye() ); } - public function test_spy_on_method_for_existing_method_stub_does_not_override_method() { + 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() ); @@ -95,12 +102,18 @@ public function test_spy_on_method_for_a_delegate_instance_which_was_overridden_ $this->assertTrue( $spy->was_called() ); } - public function test_spy_on_method_for_a_delegate_instance_does_not_override_the_instance_method() { + 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' ); @@ -116,7 +129,7 @@ public function test_spy_on_method_for_existing_method_stub_returns_spy_which_is $this->assertTrue( $spy->was_called() ); } - public function test_spy_on_method_for_existing_real_method_returns_spy_which_is_triggered_by_existing_method() { + 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(); From 2c6558aff0edd751a06a3ee7814cae72b11427ab Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 18:41:24 -0400 Subject: [PATCH 07/21] Tests: Add tests for args passes through to delegates --- tests/MockObjectTest.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index b6d7dde..5e5eac2 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; + } } /** @@ -88,12 +92,37 @@ public function test_mock_object_with_instance_delegates_methods_to_instance_met $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_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_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' ); From ce67f9fd0e4472e712be1572a7523dfa11882b2c Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 18:58:34 -0400 Subject: [PATCH 08/21] Tests: add additional tests for add_method with functions --- tests/MockObjectTest.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index 5e5eac2..84881a0 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -103,6 +103,14 @@ public function test_add_method_on_a_delegate_instance_overrides_the_instance_me $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' ); @@ -123,6 +131,26 @@ public function test_add_method_with_return_function_on_a_delegate_instance_over $this->assertEquals( 'cool is cool', $mock->just_say( 'cool' ) ); } + 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' ); From e5cf00dd3ab2762888d23723457b388328b1f8ef Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 20:23:53 -0400 Subject: [PATCH 09/21] Add Helpers::make_spy_from_function --- src/Spies/Helpers.php | 6 ++++++ 1 file changed, 6 insertions(+) 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; + } } From 045620c6fa9cf894edcd35f4b5a4f1bf1d792c63 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 20:24:39 -0400 Subject: [PATCH 10/21] Tests: add test for function args on a class mock --- tests/MockObjectTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index 84881a0..aed1af7 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -131,6 +131,14 @@ public function test_add_method_with_return_function_on_a_delegate_instance_over $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 ) { From 8a06d98c13bcd40c52bb54646127b72a9ee1ea7b Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 20:25:15 -0400 Subject: [PATCH 11/21] Refactor add_method to wrap all functions in a Spy --- src/Spies/MockObject.php | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index c02f771..77c4463 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -3,7 +3,6 @@ class MockObject { - private $spies_on_methods = []; private $class_name = null; private $delegate_instance = null; private $ignore_missing_methods = false; @@ -80,9 +79,6 @@ public function __call( $function_name, $args ) { * @return Spy|callback The Spy or callback */ public function spy_on_method( $function_name, $function = null ) { - if ( isset( $this->spies_on_methods[ $function_name ] ) ) { - return $this->spies_on_methods[ $function_name ]; - } return $this->add_method( $function_name, $function ); } @@ -106,7 +102,7 @@ public function spy_on_method( $function_name, $function = null ) { */ 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', @@ -119,17 +115,14 @@ public function add_method( $function_name, $function = null ) { if ( ! is_callable( $function ) ) { throw new \Exception( 'The function "' . $function_name . '" added to this mock object was not a function' ); } - if ( $function instanceof Spy ) { - $function->set_function_name( $function_name ); - if ( $this->delegate_instance ) { - $function->will_return( function() use ( $function_name ) { - return call_user_func_array( [ $this->delegate_instance, $function_name ], func_get_args() ); - } ); - } + if ( $function instanceof Spy && $this->delegate_instance ) { + $function->will_return( [ $this->delegate_instance, $function_name ] ); + } + if ( ! $function instanceof Spy ) { + $function = Helpers::make_spy_from_function( $function ); } - $function_spy = ( $function instanceof Spy ) ? $function : new \Spies\Spy(); + $function->set_function_name( $function_name ); $this->$function_name = $function; - $this->spies_on_methods[ $function_name ] = $function_spy; return $function; } From 9c21f6c8154c0df5dd276d3280b33d50f29c71e5 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 20:28:06 -0400 Subject: [PATCH 12/21] Adjust phpdoc for MockObject --- src/Spies/MockObject.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index 77c4463..38c5c03 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -70,13 +70,11 @@ public function __call( $function_name, $args ) { /** * Spy on a method on this object * - * If the method does not already exist, this is an alias for `add_method()`. - * If the method _does_ exist, this will install a Spy on the existing method - * and return that Spy. + * 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 ); @@ -97,8 +95,8 @@ 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 ) ) { From 79bb05e426e264d5f4697a01128857ad95ac8ce9 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 16 Oct 2017 20:38:33 -0400 Subject: [PATCH 13/21] MockObject: clean up constructor --- src/Spies/MockObject.php | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index 38c5c03..25e0d3e 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -44,19 +44,24 @@ public function __construct( $instance_or_class_name = null ) { return; } if ( is_string( $instance_or_class_name ) ) { - $class_name = $instance_or_class_name; - $this->class_name = $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 ) ); - return; + return $this->create_mock_object_for_class( $instance_or_class_name ); } - $instance = $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 ( ! 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 ) { if ( ! isset( $this->$function_name ) || ! is_callable( $this->$function_name ) ) { if ( $this->ignore_missing_methods ) { From dba40351f917ab793ac2ceaa96d106c91bfeb2cc Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 13:46:48 -0400 Subject: [PATCH 14/21] Add delegate docs to README --- README.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/README.md b/README.md index 4464e2a..e1b69b4 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. From 626a443265cfce01a917f41a5cff7124eb301c6f Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 13:50:06 -0400 Subject: [PATCH 15/21] Change API docs to use headings for functions --- API.md | 272 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 192 insertions(+), 80 deletions(-) diff --git a/API.md b/API.md index d9db280..a842ac4 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,9 @@ $expectation = expect_spy( $spy )->to_have_been_called(); $expectation->verify(); ``` -- `mock_object()`: Shortcut for `MockObject::mock_object()`. +### `mock_object()` + +Shortcut for `MockObject::mock_object()`. ```php $obj = mock_object(); @@ -49,7 +61,9 @@ $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 )`. +### `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 +74,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 +85,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 +96,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 +113,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 +130,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 +140,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 +166,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 +177,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 +187,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 +197,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 +207,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 +218,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 +229,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 +239,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 +249,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 +261,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 +272,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 +284,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 +296,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 +307,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 +319,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 +334,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 +362,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 +373,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 +415,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 +427,13 @@ $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()`. ```php $obj = Spies\MockObject::mock_object(); @@ -363,7 +441,9 @@ $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`. +### `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 +456,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 +468,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 +480,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 +506,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 +517,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 +528,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 +539,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 +550,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 +561,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 +572,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 +583,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 +596,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 +608,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 +619,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 +631,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 +644,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 +666,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 +681,7 @@ say_hello( 'friend' ); $this->assertSpyWasCalledWith( $spy, [ 'friend' ] ); ``` -- `assertSpyWasNotCalledWith( $spy, $args )` +### `assertSpyWasNotCalledWith( $spy, $args )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -577,7 +689,7 @@ say_hello( 'robot' ); $this->assertSpyWasNotCalledWith( $spy, [ 'friend' ] ); ``` -- `assertSpyWasCalledTimes( $spy, $count )` +### `assertSpyWasCalledTimes( $spy, $count )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -586,7 +698,7 @@ say_hello( 'robot' ); $this->assertSpyWasCalledTimes( $spy, 2 ); ``` -- `assertSpyWasNotCalledTimes( $spy, $count )` +### `assertSpyWasNotCalledTimes( $spy, $count )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -595,7 +707,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 +716,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 +725,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 +735,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 +745,7 @@ say_hello(); $this->assertSpyWasNotCalledBefore( $spy, $other_spy ); ``` -- `assertSpyWasCalledWhen( $spy, $callable )` +### `assertSpyWasCalledWhen( $spy, $callable )` ```php $spy = Spy::get_spy_for( 'say_hello' ); @@ -643,7 +755,7 @@ $this->assertSpyWasCalledWhen( $spy, function( $args ) { } ); ``` -- `assertSpyWasNotCalledWhen( $spy, $callable )` +### `assertSpyWasNotCalledWhen( $spy, $callable )` ```php $spy = Spy::get_spy_for( 'say_hello' ); From 6004e9edc706424b99aff9f2f7dcbdd759a83773 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 13:52:24 -0400 Subject: [PATCH 16/21] Fix typo in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e1b69b4..7e68018 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ function test_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->assertEquals( 'goodbye', $mock->say_goodbye() ); $this->assertSpyWasCalled( $say_goodbye ); } ``` From 1792584f91c2c192c1610d2048e5b9e6d2345084 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 13:55:37 -0400 Subject: [PATCH 17/21] Add delegate docs to API --- API.md | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/API.md b/API.md index a842ac4..dfe7f73 100644 --- a/API.md +++ b/API.md @@ -53,7 +53,8 @@ $expectation->verify(); ### `mock_object()` -Shortcut for `MockObject::mock_object()`. +Shortcut for `MockObject::mock_object()`. Can also be used to create a +mock object with a delegate. ```php $obj = mock_object(); @@ -61,6 +62,15 @@ $obj->add_method( 'run' ); $obj->run(); ``` +```php +$obj = \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 )`. @@ -433,7 +443,9 @@ $this->assertGreaterThan( $now, $calls[0]->get_timestamp() ); ### `mock_object()` -Shortcut for `new MockObject()`. +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(); @@ -441,6 +453,29 @@ $obj->add_method( 'run' ); $obj->run(); ``` +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`. From 9f03ccbad5f446046f3a8a95045505f645d9d4c7 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 13:57:39 -0400 Subject: [PATCH 18/21] Fix typo in API --- API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/API.md b/API.md index dfe7f73..7be52d7 100644 --- a/API.md +++ b/API.md @@ -63,7 +63,7 @@ $obj->run(); ``` ```php -$obj = \Spies\mock_object( new 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() ); From 6018a01f40772b80663dd74880c95f06fe9049c1 Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 14:05:21 -0400 Subject: [PATCH 19/21] Add InvalidFunctionNameException --- src/Spies/InvalidFunctionNameException.php | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/Spies/InvalidFunctionNameException.php 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 @@ + Date: Mon, 23 Oct 2017 14:10:47 -0400 Subject: [PATCH 20/21] Tests: add tests for throwing exceptions in MockObject --- tests/MockObjectTest.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/MockObjectTest.php b/tests/MockObjectTest.php index aed1af7..0693eb1 100644 --- a/tests/MockObjectTest.php +++ b/tests/MockObjectTest.php @@ -227,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 ); + } } From f5429575d80d71f1700873934908f9985e7299cb Mon Sep 17 00:00:00 2001 From: Payton Swick Date: Mon, 23 Oct 2017 14:12:10 -0400 Subject: [PATCH 21/21] Throw specific Exceptions when add_method fails --- src/Spies/MockObject.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Spies/MockObject.php b/src/Spies/MockObject.php index 25e0d3e..e86c863 100644 --- a/src/Spies/MockObject.php +++ b/src/Spies/MockObject.php @@ -113,10 +113,10 @@ public function add_method( $function_name, $function = null ) { 'and_ignore_missing', ]; if ( in_array( $function_name, $reserved_method_names ) ) { - throw new \Exception( 'The function "' . $function_name . '" added to this mock object could not be used because it conflicts with a built-in function' ); + 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 ] );