diff --git a/API.md b/API.md index 952c1df..e6579fd 100644 --- a/API.md +++ b/API.md @@ -91,3 +91,28 @@ - `once()`: Alias for `times( 1 )`. - `twice()`: Alias for `times( 2 )`. - `before( $spy )`: Add an expected behavior that the spy was called before $spy. + +# PHPUnit Custom Assertions + +These are methods available on instances of `\Spies\TestCase`. + +### Constraints for `assertThat()` + +- `wasCalled()` +- `wasNotCalled()` +- `wasCalledTimes( $count )` +- `wasCalledBefore( $spy )` +- `wasCalledWhen( $callable )` + +### Assertions + +- `assertSpyWasCalled( $spy )` +- `assertSpyWasNotCalled( $spy )` +- `assertSpyWasCalledWith( $spy, $args )` +- `assertSpyWasNotCalledWith( $spy, $args )` +- `assertSpyWasCalledTimes( $spy, $count )` +- `assertSpyWasNotCalledTimes( $spy, $count )` +- `assertSpyWasCalledBefore( $spy, $other_spy )` +- `assertSpyWasNotCalledBefore( $spy, $other_spy )` +- `assertSpyWasCalledWhen( $spy, $callable )` +- `assertSpyWasNotCalledWhen( $spy, $callable )` diff --git a/README.md b/README.md index d00c756..827712c 100644 --- a/README.md +++ b/README.md @@ -316,3 +316,29 @@ function test_calculation() { add_together( 2, 3 ); } ``` + +## PHPUnit Custom Assertions + +If you prefer to use PHPUnit custom assertions rather than Expectations, those are also available (although you must base your test class on `\Spies\TestCase`): + +```php +class MyTest extends \Spies\TestCase { + function test_spy_is_called_correctly() { + $spy = \Spies\make_spy(); + $spy( 'hello', 'world', 7 ); + $spy( 'hello', 'world', 8 ); + $this->assertSpyWasCalledWith( 'hello', 'world', \Spies\any() ); + } +} +``` + +Custom assertions will provide detailed information about why your test failed, which is much better than "false is not true". + +``` +Failed asserting that a spy is called with arguments: ( "a", "b", "c" ). +a spy was actually called with: + 1. arguments: ( "b", "b", "c" ), + 2. arguments: ( "m", "b", "c" ) +``` + +See the [API document](API.md) for the full list of custom assertions available. diff --git a/src/Spies/ArgumentFormatter.php b/src/Spies/ArgumentFormatter.php new file mode 100644 index 0000000..591b093 --- /dev/null +++ b/src/Spies/ArgumentFormatter.php @@ -0,0 +1,21 @@ +args = $args; + } + + public function __toString() { + if ( empty( $this->args ) ) { + return 'no arguments'; + } + return 'arguments: ( ' . $this->get_args_as_array() . ' )'; + } + + private function get_args_as_array() { + return implode( ', ', array_map( 'json_encode', $this->args ) ); + } +} diff --git a/src/Spies/FailureGenerator.php b/src/Spies/FailureGenerator.php new file mode 100644 index 0000000..bec1fc1 --- /dev/null +++ b/src/Spies/FailureGenerator.php @@ -0,0 +1,56 @@ +messages[] = $message; + } + + public function get_message() { + return implode( ' ', $this->messages ); + } + + public function spy_was_not_called( $spy ) { + $this->add_message( $spy->get_function_name() . ' is called' ); + } + + public function spy_was_called( $spy ) { + $this->add_message( $spy->get_function_name() . ' is not called' ); + } + + public function spy_was_not_called_with( $spy, $args ) { + $this->spy_was_not_called( $spy ); + $desc = 'with '; + $desc .= strval( new ArgumentFormatter( $args ) ); + $this->add_message( $desc ); + } + + public function spy_was_not_called_with_additional( $spy ) { + $desc = $spy->get_function_name() . ' was actually '; + $calls = $spy->get_called_functions(); + $desc .= empty( $calls ) ? 'not called at all' : 'called with:' . strval( new SpyCallFormatter( $calls ) ); + $this->add_message( $desc ); + } + + public function spy_was_not_called_times( $spy, $count ) { + $this->spy_was_not_called( $spy ); + $desc = $count . ' '; + $desc .= $count === 1 ? 'time' : 'times'; + $this->add_message( $desc ); + } + + public function spy_was_not_called_before( $spy, $target_spy ) { + $this->spy_was_not_called( $spy ); + $desc = 'before ' . $target_spy->get_function_name(); + $this->add_message( $desc ); + } + + public function spy_was_not_called_when( $spy ) { + $this->spy_was_not_called( $spy ); + $desc = 'with arguments matching the provided function'; + $this->add_message( $desc ); + } + +} diff --git a/src/Spies/SpiesConstraintWasCalled.php b/src/Spies/SpiesConstraintWasCalled.php new file mode 100644 index 0000000..a27411f --- /dev/null +++ b/src/Spies/SpiesConstraintWasCalled.php @@ -0,0 +1,27 @@ +was_called(); + } + + public function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called( $other ); + return $generator->get_message(); + } + + protected function additionalFailureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_with_additional( $other ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} diff --git a/src/Spies/SpiesConstraintWasCalledBefore.php b/src/Spies/SpiesConstraintWasCalledBefore.php new file mode 100644 index 0000000..0e0c895 --- /dev/null +++ b/src/Spies/SpiesConstraintWasCalledBefore.php @@ -0,0 +1,30 @@ +target_spy = $target_spy; + } + + public function matches( $other ) { + if ( ! $other instanceof \Spies\Spy ) { + return false; + } + return $other->was_called_before( $this->target_spy ); + } + + public function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_before( $other, $this->target_spy ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} + + diff --git a/src/Spies/SpiesConstraintWasCalledTimes.php b/src/Spies/SpiesConstraintWasCalledTimes.php new file mode 100644 index 0000000..46e03a9 --- /dev/null +++ b/src/Spies/SpiesConstraintWasCalledTimes.php @@ -0,0 +1,35 @@ +count = $count; + } + + public function matches( $other ) { + if ( ! $other instanceof \Spies\Spy ) { + return false; + } + return $other->was_called_times( $this->count ); + } + + public function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_times( $other, $this->count ); + return $generator->get_message(); + } + + protected function additionalFailureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_with_additional( $other ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} + diff --git a/src/Spies/SpiesConstraintWasCalledWhen.php b/src/Spies/SpiesConstraintWasCalledWhen.php new file mode 100644 index 0000000..2cc443c --- /dev/null +++ b/src/Spies/SpiesConstraintWasCalledWhen.php @@ -0,0 +1,35 @@ +expected_callable = $callable; + } + + public function matches( $other ) { + if ( ! $other instanceof \Spies\Spy ) { + return false; + } + return $other->was_called_when( $this->expected_callable ); + } + + protected function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_when( $other ); + return $generator->get_message(); + } + + protected function additionalFailureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_with_additional( $other ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} + diff --git a/src/Spies/SpiesConstraintWasCalledWith.php b/src/Spies/SpiesConstraintWasCalledWith.php new file mode 100644 index 0000000..ff4875b --- /dev/null +++ b/src/Spies/SpiesConstraintWasCalledWith.php @@ -0,0 +1,35 @@ +expected_args = $args; + } + + public function matches( $other ) { + if ( ! $other instanceof \Spies\Spy ) { + return false; + } + return $other->was_called_with_array( $this->expected_args ); + } + + protected function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_with( $other, $this->expected_args ); + return $generator->get_message(); + } + + protected function additionalFailureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_not_called_with_additional( $other ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} + diff --git a/src/Spies/SpiesConstraintWasNotCalled.php b/src/Spies/SpiesConstraintWasNotCalled.php new file mode 100644 index 0000000..0dfb419 --- /dev/null +++ b/src/Spies/SpiesConstraintWasNotCalled.php @@ -0,0 +1,22 @@ +was_called(); + } + + public function failureDescription( $other ) { + $generator = new FailureGenerator(); + $generator->spy_was_called( $other ); + return $generator->get_message(); + } + + public function toString() { + return ''; + } +} + diff --git a/src/Spies/Spy.php b/src/Spies/Spy.php index 9944550..ee0af8c 100644 --- a/src/Spies/Spy.php +++ b/src/Spies/Spy.php @@ -18,8 +18,19 @@ public function __construct( $function_name = null ) { $this->function_name = $function_name; } + public function __toString() { + $summary = []; + $i = 0; + $summary[] = empty( $this->call_record ) ? 'never called' : "calls:\n " . implode( ",\n ", array_map( function( $cr ) use ( $i ) { + $i++; + return $i . '. ' . strval( $cr ); + }, $this->call_record ) ) . "\n"; + ( ! empty( $this->function_name ) ) && $summary['function_name'] = $this->function_name; + return '\Spies\Spy(' . implode( $summary ) . ')'; + } + /** - * Allows Spy to be called as a function + * Allows Spy to be called as a function * * Alias for `call` */ @@ -70,7 +81,7 @@ public static function passed_arg( $index ) { } /** - * Return the function name + * Return the function name * * @return string The function name */ @@ -78,7 +89,7 @@ public function get_function_name() { if ( isset( $this->function_name ) ) { return $this->function_name; } - return 'anonymous function'; + return 'a spy'; } public function set_function_name( $function_name ) { @@ -101,7 +112,7 @@ public function call() { } /** - * Call this mocked function with an array of arguments. + * Call this mocked function with an array of arguments. * * Same as `call`, but with an array of arguments instead of any number. * @@ -114,7 +125,7 @@ public function call_with_array( $args ) { } /** - * Clear the record of calls for this spy + * Clear the record of calls for this spy */ public function clear_call_record() { $this->call_record = []; @@ -198,7 +209,7 @@ public function that_returns( $value ) { * mock_function( 'add_one' )->and_return( function( $a ) { * return $a + 1; * } ); - * + * * @param mixed $value The value to return when this spy is called * @return Spy This Spy */ @@ -219,7 +230,7 @@ public function and_return( $value ) { * particular value if certain arguments are present when the function is * called. * - * @param mixed $arg... The arguments to use when defining a behavior + * @param mixed $arg... The arguments to use when defining a behavior * @return Spy This spy */ public function with() { @@ -237,7 +248,7 @@ public function get_times_called() { } /** - * Return the call record for a single call + * Return the call record for a single call * * @param integer $index The 0-based index of the call record to return * @return array|null The call record @@ -247,7 +258,7 @@ public function get_call( $index ) { } /** - * Return true if the spy was called + * Return true if the spy was called * * @return boolean True if the spy was called */ @@ -256,7 +267,7 @@ public function was_called() { } /** - * Return true if the spy was called a certain number of times + * Return true if the spy was called a certain number of times * * @param integer $times The number of times the function should have been called * @return boolean True if the spy was called $times times @@ -266,13 +277,14 @@ public function was_called_times( $times ) { } /** - * Return true if the spy was called with a certain number of arguments + * Return true if the spy was called with a certain number of arguments * - * @param mixed $arg... The arguments to look for in the call record + * Array version of was_called_with + * + * @param array $arg The arguments to look for in the call record * @return boolean True if the spy was called with the arguments */ - public function was_called_with() { - $args = func_get_args(); + public function was_called_with_array( $args ) { $matching_calls = array_filter( $this->get_called_functions(), function( $call ) use ( $args ) { return ( Helpers::do_args_match( $call->get_args(), $args ) ); } ); @@ -280,7 +292,7 @@ public function was_called_with() { } /** - * Return true if a spy call causes a function to return true + * Return true if a spy call causes a function to return true * * @param callable $callable A function to call with every set of arguments * @return boolean True if the callable function matches at least one set of arguments @@ -292,6 +304,16 @@ public function was_called_when( $callable ) { return ( count( $matching_calls ) > 0 ); } + /** + * Return true if the spy was called with a certain number of arguments + * + * @param mixed $arg... The arguments to look for in the call record + * @return boolean True if the spy was called with the arguments + */ + public function was_called_with() { + return $this->was_called_with_array( func_get_args() ); + } + public function was_called_before( $spy ) { $call_record = $this->get_called_functions(); if ( count( $call_record ) < 1 ) { @@ -311,7 +333,7 @@ private function set_arguments( $args ) { } /** - * Add a function call to the call record + * Add a function call to the call record * * You should not need to call this directly. */ diff --git a/src/Spies/SpyCall.php b/src/Spies/SpyCall.php index c790db4..246d629 100644 --- a/src/Spies/SpyCall.php +++ b/src/Spies/SpyCall.php @@ -27,4 +27,8 @@ public function get_args() { public function get_timestamp() { return $this->timestamp; } + + public function __toString() { + return strval( new ArgumentFormatter( $this->get_args() ) ); + } } diff --git a/src/Spies/SpyCallFormatter.php b/src/Spies/SpyCallFormatter.php new file mode 100644 index 0000000..c9d9da9 --- /dev/null +++ b/src/Spies/SpyCallFormatter.php @@ -0,0 +1,18 @@ +calls = $calls; + } + + public function __toString() { + $i = 0; + return "\n " . implode( ",\n ", array_map( function( $call ) use ( &$i ) { + $i++; + return $i . '. ' . strval( $call ); + }, $this->calls ) ) . "\n"; + } +} diff --git a/src/Spies/TestCase.php b/src/Spies/TestCase.php new file mode 100644 index 0000000..cd9fc8c --- /dev/null +++ b/src/Spies/TestCase.php @@ -0,0 +1,70 @@ +assertSpyWasCalled( $spy ); + } + + public function test_assert_was_not_called_is_true_when_not_called() { + $spy = \Spies\make_spy(); + $this->assertSpyWasNotCalled( $spy ); + } + + public function test_assert_that_was_called_is_true_when_called() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->wasCalled() ); + } + + public function test_assert_that_was_called_is_true_when_not_called() { + $spy = \Spies\make_spy(); + $this->assertThat( $spy, $this->wasNotCalled() ); + } + + public function test_assert_that_logical_not_was_called_is_true_when_not_called() { + $spy = \Spies\make_spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalled() ) ); + } + + public function test_assert_that_was_called_with_is_true_when_called_with_args() { + $spy = \Spies\make_spy(); + $spy( 'a', 'b', 'c' ); + $this->assertThat( $spy, $this->wasCalledWith( [ 'a', 'b', 'c' ] ) ); + } + + public function test_assert_that_was_logical_not_called_with_is_true_when_not_called_with_args() { + $spy = \Spies\make_spy(); + $spy( 'b', 'b', 'c' ); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledWith( [ 'a', 'b', 'c' ] ) ) ); + } + + public function test_assert_spy_was_called_with_is_true_when_called_with_args() { + $spy = \Spies\make_spy(); + $spy( 'a', 'b', 'c' ); + $this->assertSpyWasCalledWith( $spy, [ 'a', 'b', 'c' ] ); + } + + public function test_assert_spy_was_not_called_with_is_true_when_not_called_with_args() { + $spy = \Spies\make_spy(); + $spy( 'e', 'b', 'c' ); + $this->assertSpyWasNotCalledWith( $spy, [ 'a', 'b', 'c' ] ); + } + + public function test_assert_was_called_times_is_true_when_called_once() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->wasCalledTimes( 1 ) ); + } + + public function test_assert_was_called_times_is_true_when_called_twice() { + $spy = \Spies\make_spy(); + $spy(); + $spy(); + $this->assertThat( $spy, $this->wasCalledTimes( 2 ) ); + } + + public function test_assert_was_called_times_is_false_when_called_less_than_number() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledTimes( 2 ) ) ); + } + + public function test_assert_was_called_times_is_false_when_called_more_than_number() { + $spy = \Spies\make_spy(); + $spy(); + $spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledTimes( 1 ) ) ); + } + + public function test_assert_spy_was_called_times_is_true_when_called_that_number() { + $spy = \Spies\make_spy(); + $spy(); + $spy(); + $spy(); + $this->assertSpyWasCalledTimes( $spy, 3 ); + } + + public function test_assert_spy_was_not_called_times_is_true_when_not_called_that_number() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertSpyWasNotCalledTimes( $spy, 3 ); + } + + public function test_assert_that_spy_was_called_before_is_true_when_called_before_other_spy() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy(); + $spy2(); + $this->assertThat( $spy, $this->wasCalledBefore( $spy2 ) ); + } + + public function test_assert_that_spy_was_called_before_is_false_when_called_after_other_spy() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy2(); + $spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledBefore( $spy2 ) ) ); + } + + public function test_assert_that_spy_was_called_before_is_false_when_other_spy_not_called() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledBefore( $spy2 ) ) ); + } + + public function test_assert_that_spy_was_called_before_is_false_when_spy_not_called() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy2(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledBefore( $spy2 ) ) ); + } + + public function test_assert_spy_was_called_before_is_true_when_called_before_other_spy() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy(); + $spy2(); + $this->assertSpyWasCalledBefore( $spy, $spy2 ); + } + + public function test_assert_spy_was_not_called_before_is_true_when_called_after_other_spy() { + $spy = \Spies\make_spy(); + $spy2 = \Spies\make_spy(); + $spy2(); + $spy(); + $this->assertSpyWasNotCalledBefore( $spy, $spy2 ); + } + + public function test_assert_that_spy_was_called_when_is_true_when_called_function_returns_true() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->wasCalledWhen( function() { + return true; + } ) ); + } + + public function test_assert_that_spy_was_called_when_is_false_when_called_function_returns_false() { + $spy = \Spies\make_spy(); + $spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledWhen( function() { + return false; + } ) ) ); + } + + public function test_assert_that_spy_was_called_when_is_false_when_spy_was_not_called() { + $spy = \Spies\make_spy(); + $this->assertThat( $spy, $this->logicalNot( $this->wasCalledWhen( function() { + return true; + } ) ) ); + } + + public function test_assert_that_spy_was_called_when_function_is_called_with_spy_args() { + $spy = \Spies\make_spy(); + $spy( 'hello', 'world' ); + $this->assertThat( $spy, $this->wasCalledWhen( function( $args ) { + return ( $args === [ 'hello', 'world' ] ); + } ) ); + } + + public function test_assert_that_spy_was_called_when_function_is_called_for_each_call() { + $spy = \Spies\make_spy(); + $spy( 1 ); + $spy( 2 ); + $count = 0; + $this->assertThat( $spy, $this->wasCalledWhen( function() use ( &$count ) { + $count ++; + return true; + } ) ); + $this->assertEquals( 2, $count ); + } + + public function test_assert_spy_was_called_when_is_true_when_called_function_returns_true() { + $spy = \Spies\make_spy(); + $spy( 'hi' ); + $spy( 'yo' ); + $this->assertSpyWasCalledWhen( $spy, function( $args ) { + return ( $args === [ 'yo' ] ); + } ); + } + + public function test_assert_spy_was_called_when_is_false_when_called_function_returns_false() { + $spy = \Spies\make_spy(); + $spy(); + $spy( 'yo' ); + $this->assertSpyWasNotCalledWhen( $spy, function( $args ) { + return ( $args === [ 'hi' ] ); + } ); + } +} diff --git a/tests/SpyTest.php b/tests/SpyTest.php index 6caea86..86d200b 100644 --- a/tests/SpyTest.php +++ b/tests/SpyTest.php @@ -86,6 +86,18 @@ public function test_spy_was_called_times_returns_false_if_the_argument_does_not $this->assertFalse( $spy->was_called_times( 6 ) ); } + public function test_spy_was_called_with_array_returns_true_if_the_spy_was_called_with_the_arguments_provided() { + $spy = \Spies\make_spy(); + $spy( 'foo', 'bar', 'baz' ); + $this->assertTrue( $spy->was_called_with_array( [ 'foo', 'bar', 'baz' ] ) ); + } + + public function test_spy_was_called_with_array_returns_false_if_the_spy_was_not_called_with_the_arguments_provided() { + $spy = \Spies\make_spy(); + $spy( 'foo' ); + $this->assertFalse( $spy->was_called_with( [ 'foo', 'bar', 'baz' ] ) ); + } + public function test_spy_was_called_with_returns_true_if_the_spy_was_called_with_the_arguments_provided() { $spy = \Spies\make_spy(); $spy( 'foo', 'bar', 'baz' );