├── .github
├── dependabot.yml
└── workflows
│ └── main.yml
├── .gitignore
├── art
├── header.png
├── logo@1x.png
├── logo@2x.png
├── logo@3x.png
├── logo@4x.png
└── socialcard.png
├── composer.json
├── infection.json5
├── license.txt
├── phpstan.neon
├── phpunit.10.xml
├── phpunit.xml
├── readme.md
├── src
└── CallableFake.php
└── tests
└── CallableFakeTest.php
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: composer
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "19:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | jobs:
12 | static-analysis:
13 | name: Static analysis
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v4
19 |
20 | - name: Get composer cache directory
21 | id: composer-cache
22 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
23 |
24 | - name: Cache dependencies
25 | uses: actions/cache@v4
26 | with:
27 | path: ${{ steps.composer-cache.outputs.dir }}
28 | key: ${{ runner.os }}-php-8.2-composer-${{ hashFiles('**/composer.json') }}
29 | restore-keys: ${{ runner.os }}-php8.2-composer-
30 |
31 | - name: Setup PHP
32 | uses: shivammathur/setup-php@v2
33 | with:
34 | php-version: 8.2
35 | tools: phpstan
36 | coverage: none
37 |
38 | - name: Install dependencies
39 | run: composer install
40 |
41 | - name: Check platform requirements
42 | run: composer check-platform-reqs
43 |
44 | - name: PHPStan
45 | run: phpstan
46 |
47 |
48 | code-style:
49 | name: Code style
50 | runs-on: ubuntu-latest
51 |
52 | steps:
53 | - name: Checkout code
54 | uses: actions/checkout@v4
55 |
56 | - name: Setup PHP
57 | uses: shivammathur/setup-php@v2
58 | with:
59 | php-version: 8.4
60 | ## Temporary version pin as 1.21 is broken
61 | tools: pint:1.20.0
62 | coverage: none
63 |
64 | - name: Pint
65 | run: pint --test
66 |
67 |
68 | mutation-tests:
69 | name: Mutation tests
70 | runs-on: ubuntu-latest
71 |
72 | steps:
73 | - name: Checkout code
74 | uses: actions/checkout@v4
75 |
76 | - name: Get composer cache directory
77 | id: composer-cache
78 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
79 |
80 | - name: Cache dependencies
81 | uses: actions/cache@v4
82 | with:
83 | path: ${{ steps.composer-cache.outputs.dir }}
84 | key: ${{ runner.os }}-php-8.4-composer-${{ hashFiles('**/composer.json') }}
85 | restore-keys: ${{ runner.os }}-php8.4-composer-
86 |
87 | - name: Setup PHP
88 | uses: shivammathur/setup-php@v2
89 | with:
90 | php-version: 8.4
91 | tools: infection
92 | coverage: pcov
93 |
94 | - name: Install dependencies
95 | run: composer install
96 |
97 | - name: Infection
98 | run: infection --show-mutations
99 |
100 |
101 | tests:
102 | runs-on: ubuntu-latest
103 | name: PHP ${{ matrix.php }} PHPUnit ${{ matrix.phpunit }}
104 | strategy:
105 | matrix:
106 | php: [8.2, 8.3, 8.4]
107 | phpunit: [10, 11, 12]
108 | exclude:
109 | - phpunit: 12
110 | php: 8.2
111 |
112 | steps:
113 | - name: Checkout code
114 | uses: actions/checkout@v4
115 |
116 | - name: Get composer cache directory
117 | id: composer-cache
118 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
119 |
120 | - name: Cache dependencies
121 | uses: actions/cache@v4
122 | with:
123 | path: ${{ steps.composer-cache.outputs.dir }}
124 | key: ${{ runner.os }}-php-${{ matrix.php }}-phpunit-${{ matrix.phpunit }}-composer-${{ hashFiles('**/composer.json') }}
125 | restore-keys: ${{ runner.os }}-php-${{ matrix.php }}-phpunit-${{ matrix.phpunit }}-composer-
126 |
127 | - name: Setup PHP
128 | uses: shivammathur/setup-php@v2
129 | with:
130 | php-version: ${{ matrix.php }}
131 | coverage: none
132 |
133 | - name: Install dependencies
134 | run: |
135 | composer require --no-update \
136 | phpunit/phpunit:^${{ matrix.phpunit }}
137 | composer update
138 |
139 | - name: Configure PHPUnit
140 | run: if [ -f "./phpunit.${{ matrix.phpunit }}.xml" ]; then cp ./phpunit.${{ matrix.phpunit }}.xml ./phpunit.xml; fi
141 |
142 | - name: PHPUnit
143 | run: ./vendor/bin/phpunit
144 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor
2 | /composer.lock
3 | /.phpunit.cache
4 |
--------------------------------------------------------------------------------
/art/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/header.png
--------------------------------------------------------------------------------
/art/logo@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/logo@1x.png
--------------------------------------------------------------------------------
/art/logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/logo@2x.png
--------------------------------------------------------------------------------
/art/logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/logo@3x.png
--------------------------------------------------------------------------------
/art/logo@4x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/logo@4x.png
--------------------------------------------------------------------------------
/art/socialcard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/timacdonald/callable-fake/612fbb3dbb603f0ffe54e268c90e74b33b49a67b/art/socialcard.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "timacdonald/callable-fake",
3 | "description": "A testing utility that allows you to fake and capture invokations of a callable / Closure",
4 | "keywords": [
5 | "callable",
6 | "Closure",
7 | "fake",
8 | "testing"
9 | ],
10 | "license": "MIT",
11 | "authors": [
12 | {
13 | "name": "Tim MacDonald",
14 | "email": "hello@timacdonald.me",
15 | "homepage": "https://timacdonald.me"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.2",
20 | "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0"
21 | },
22 | "autoload": {
23 | "psr-4": {
24 | "TiMacDonald\\CallableFake\\": "src"
25 | }
26 | },
27 | "autoload-dev": {
28 | "psr-4": {
29 | "Tests\\": "tests/"
30 | }
31 | },
32 | "config": {
33 | "preferred-install": "dist",
34 | "sort-packages": true
35 | },
36 | "minimum-stability": "stable",
37 | "prefer-stable": true
38 | }
39 |
--------------------------------------------------------------------------------
/infection.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "vendor/infection/infection/resources/schema.json",
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | "mutators": {
9 | "@default": true,
10 | "CastBool": {
11 | "ignoreSourceCodeByRegex": [
12 | "return \\(bool\\) \\$callback\\(\\.\\.\\.\\$arguments\\);"
13 | ],
14 | }
15 | },
16 | "minMsi": 100
17 | }
18 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Tim MacDonald
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 | level: max
3 | paths:
4 | - src
5 | - tests
6 |
--------------------------------------------------------------------------------
/phpunit.10.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | tests
14 |
15 |
16 |
17 |
18 |
19 | src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | tests
14 |
15 |
16 |
17 |
18 |
19 | src
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | # Callable / Closure testing fake
4 |
5 | If you have an interface who's public API allows a developer to pass a Closure / callable, but causes no internal or external side-effects, as these are left up to the developer using the interface, this package may assist in testing. This class adds some named assertions which gives you an API that is very much inspired by Laravel's service fakes. It may be a little more verbose, but it changes the language of the tests to better reflect what is going on.
6 |
7 | It also makes it easy to assert the order of invocations, and how many times a callable has been invoked.
8 |
9 | ## Installation
10 |
11 | You can install the package using [composer](https://getcomposer.org/):
12 |
13 | ```
14 | composer require timacdonald/callable-fake --dev
15 | ```
16 |
17 | ## Basic usage
18 |
19 | This packge requires you to be testing a pretty specfic type of API / interaction to be useful. Imagine you are developing a package that ships with the following interface...
20 |
21 | ```php
22 | interface DependencyRepository
23 | {
24 | public function each(callable $callback): void;
25 | }
26 | ```
27 |
28 | This interface accepts a callback, and under the hood loops through all "dependecies" and passes each one to the callback for the developer to work with.
29 |
30 | ### Before
31 |
32 | Let's see what the a test for this method might look like...
33 |
34 | ```php
35 | public function testEachLoopsOverAllDependencies(): void
36 | {
37 | // arrange
38 | $received = [];
39 | $expected = factory(Dependency::class)->times(2)->create();
40 | $repo = $this->app[DependencyRepository::class];
41 |
42 | // act
43 | $repo->each(function (Dependency $dependency) use (&$received): void {
44 | $received[] = $dependency;
45 | });
46 |
47 | // assert
48 | $this->assertCount(2, $received);
49 | $this->assertTrue($expected[0]->is($received[0]));
50 | $this->assertTrue($expected[1]->is($received[1]));
51 | }
52 | ```
53 |
54 | ### After
55 |
56 | ```php
57 | public function testEachLoopsOverAllDependencies(): void
58 | {
59 | // arrange
60 | $callable = new CallableFake();
61 | $expected = factory(Dependency::class)->times(2)->create();
62 | $repo = $this->app[DependencyRepository::class];
63 |
64 | // act
65 | $repo->each($callable);
66 |
67 | // assert
68 | $callable->assertTimesInvoked(2);
69 | $callable->assertCalled(function (Depedency $dependency) use ($expected): bool {
70 | return $dependency->is($expected[0]);
71 | });
72 | $callable->assertCalled(function (Dependency $dependency) use ($expected): bool {
73 | return $dependency->is($expected[1]);
74 | });
75 | }
76 | ```
77 |
78 | ## Available assertions
79 |
80 | All assertions are chainable.
81 |
82 | ### assertCalled(callable $callback): self
83 |
84 | ```php
85 | $callable->assertCalled(function (Dependency $dependency): bool {
86 | return Str::startsWith($dependency->name, 'spatie/');
87 | });
88 | ```
89 |
90 | ### assertNotCalled(callable $callback): self
91 |
92 | ```php
93 | $callable->assertNotCalled(function (Dependency $dependency): bool {
94 | return Str::startsWith($dependency->name, 'timacdonald/');
95 | });
96 | ```
97 |
98 | ### assertCalledIndex(callable $callback, int|array $index): self
99 |
100 | Ensure the callable was called in an explicit order, i.e. it was called as the 0th and 5th invocation.
101 | ```php
102 | $callable->assertCalledIndex(function (Dependency $dependency): bool {
103 | return Str::startsWith($dependency, 'spatie/');
104 | }, [0, 5]);
105 | ```
106 |
107 | ### assertCalledTimes(callable $callback, int $times): self
108 |
109 | ```php
110 | $callable->assertCalledTimes(function (Dependency $dependency): bool {
111 | return Str::startsWith($dependency, 'spatie/');
112 | }, 999);
113 | ```
114 |
115 | ### assertTimesInvoked(int $times): self
116 |
117 | ```php
118 | $callable->assertTimesInvoked(2);
119 | ```
120 |
121 | ### assertInvoked(): self
122 |
123 | ```php
124 | $callable->assertInvoked();
125 | ```
126 |
127 | ### assertNotInvoked(): self
128 |
129 | ```php
130 | $callable->assertNotInvoked();
131 | ```
132 |
133 | ## Non-assertion API
134 |
135 | ### asClosure(): Closure
136 |
137 | If the method is type-hinted with `\Closure` instead of callable, you can use this method to transform the callable to an instance of `\Closure`.
138 |
139 | ```php
140 | $callable = new CallableFake;
141 |
142 | $thing->closureTypeHintedMethod($callable->asClosure());
143 |
144 | $callable->assertInvoked();
145 | ```
146 |
147 | ### wasInvoked(): bool
148 |
149 | ```php
150 | if ($callable->wasInvoked()) {
151 | //
152 | }
153 | ```
154 |
155 | ### wasNotInvoked(): bool
156 |
157 | ```php
158 | if ($callable->wasNotInvoked()) {
159 | //
160 | }
161 | ```
162 |
163 | ### called(callable $callback): array
164 |
165 | ```php
166 | $invocationArguments = $callable->called(function (Dependency $dependency): bool {
167 | return Str::startsWith($dependency->name, 'spatie/')
168 | });
169 | ```
170 |
171 | ## Specifying return values
172 |
173 | If you need to specify return values, this _could_ be an indicator that this is not the right tool for the job. But there are some cases where return values determine control flow, so it can be handy, in which case you can pass a "return resolver" to the named constructor `withReturnResolver`.
174 |
175 | ```php
176 | $callable = CallableFake::withReturnResolver(function (Dependency $dependency): bool {
177 | if ($dependency->version === '*') {
178 | return '🤠';
179 | }
180 |
181 | return '😀';
182 | });
183 |
184 | // You would not generally be calling this yourself, this is simply to demonstate
185 | // what will happen under the hood...
186 |
187 | $emoji = $callable(new Dependecy(['version' => '*']));
188 |
189 | // $emoji === '🤠';
190 | ```
191 |
192 | ## Credits
193 |
194 | - [Tim MacDonald](https://github.com/timacdonald)
195 | - [All Contributors](../../contributors)
196 |
197 | And a special (vegi) thanks to [Caneco](https://twitter.com/caneco) for the logo ✨
198 |
199 | ## Thanksware
200 |
201 | You are free to use this package, but I ask that you reach out to someone (not me) who has previously, or is currently, maintaining or contributing to an open source library you are using in your project and thank them for their work. Consider your entire tech stack: packages, frameworks, languages, databases, operating systems, frontend, backend, etc.
202 |
--------------------------------------------------------------------------------
/src/CallableFake.php:
--------------------------------------------------------------------------------
1 | >
12 | */
13 | private $invocations = [];
14 |
15 | /**
16 | * @var callable
17 | */
18 | private $returnResolver;
19 |
20 | public function __construct(?callable $callback = null)
21 | {
22 | $this->returnResolver = $callback ?? function (): void {
23 | //
24 | };
25 | }
26 |
27 | public static function withReturnResolver(callable $callback): self
28 | {
29 | return new self($callback);
30 | }
31 |
32 | /**
33 | * @return mixed
34 | */
35 | public function __invoke()
36 | {
37 | $args = func_get_args();
38 |
39 | $this->invocations[] = $args;
40 |
41 | return call_user_func_array($this->returnResolver, $args);
42 | }
43 |
44 | public function asClosure(): Closure
45 | {
46 | return Closure::fromCallable($this);
47 | }
48 |
49 | public function assertCalled(callable $callback): self
50 | {
51 | Assert::assertNotCount(0, $this->invocations, 'The callable was never called.');
52 |
53 | Assert::assertNotCount(0, $this->called($callback), 'The expected callable was not called.');
54 |
55 | return $this;
56 | }
57 |
58 | public function assertNotCalled(callable $callback): self
59 | {
60 | Assert::assertCount(0, $this->called($callback), 'An unexpected callable was called.');
61 |
62 | return $this;
63 | }
64 |
65 | public function assertCalledTimes(callable $callback, int $times): self
66 | {
67 | $timesCalled = count($this->called($callback));
68 |
69 | Assert::assertSame($times, $timesCalled, "The expected callable was called {$timesCalled} times instead of the expected {$times} times.");
70 |
71 | return $this;
72 | }
73 |
74 | /**
75 | * @param array|int $indexes
76 | */
77 | public function assertCalledIndex(callable $callback, $indexes): self
78 | {
79 | $this->assertCalled($callback);
80 |
81 | $expectedIndexes = (array) $indexes;
82 |
83 | $actualIndexes = array_keys($this->called($callback));
84 |
85 | $matches = array_intersect($expectedIndexes, $actualIndexes);
86 |
87 | Assert::assertSame(count($matches), count($expectedIndexes), 'The callable was not called in the expected order. Found at index: '.implode(', ', $actualIndexes).'. Expected to be found at index: '.implode(', ', $expectedIndexes));
88 |
89 | return $this;
90 | }
91 |
92 | public function assertTimesInvoked(int $count): self
93 | {
94 | $timesInvoked = count($this->invocations);
95 |
96 | Assert::assertSame($count, $timesInvoked, "The callable was invoked {$timesInvoked} times instead of the expected {$count} times.");
97 |
98 | return $this;
99 | }
100 |
101 | public function assertInvoked(): self
102 | {
103 | Assert::assertTrue($this->wasInvoked(), 'The callable was not invoked.');
104 |
105 | return $this;
106 | }
107 |
108 | public function assertNotInvoked(): self
109 | {
110 | Assert::assertTrue($this->wasNotInvoked(), 'The callable was invoked.');
111 |
112 | return $this;
113 | }
114 |
115 | public function wasInvoked(): bool
116 | {
117 | return count($this->invocations) > 0;
118 | }
119 |
120 | public function wasNotInvoked(): bool
121 | {
122 | return ! $this->wasInvoked();
123 | }
124 |
125 | /**
126 | * @return array>
127 | */
128 | public function called(callable $callback): array
129 | {
130 | return array_filter($this->invocations, function (array $arguments) use ($callback): bool {
131 | return (bool) $callback(...$arguments);
132 | });
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/tests/CallableFakeTest.php:
--------------------------------------------------------------------------------
1 | assertCalled(function (string $arg) {
18 | return $arg === 'x';
19 | });
20 | $this->fail();
21 | } catch (ExpectationFailedException $e) {
22 | $this->assertStringStartsWith('The callable was never called.', $e->getMessage());
23 | }
24 | }
25 |
26 | public function test_assert_called_with_true_before_being_called(): void
27 | {
28 | $fake = new CallableFake;
29 |
30 | try {
31 | $fake->assertCalled(function (string $arg) {
32 | return $arg === 'a';
33 | });
34 | $this->fail();
35 | } catch (ExpectationFailedException $e) {
36 | $this->assertStringStartsWith('The callable was never called.', $e->getMessage());
37 | }
38 | }
39 |
40 | public function test_assert_called_with_true_after_being_called(): void
41 | {
42 | $fake = new callablefake;
43 | $fake('a');
44 |
45 | $fake->assertCalled(function (string $arg) {
46 | return $arg === 'a';
47 | });
48 | }
49 |
50 | public function test_assert_called_with_false_after_being_called(): void
51 | {
52 | $fake = new CallableFake;
53 | $fake('a');
54 |
55 | try {
56 | $fake->assertCalled(function (string $arg) {
57 | return $arg === 'x';
58 | });
59 | $this->fail();
60 | } catch (ExpectationFailedException $e) {
61 | $this->assertStringStartsWith('The expected callable was not called.', $e->getMessage());
62 | }
63 | }
64 |
65 | public function test_assert_not_called_with_true_before_being_called(): void
66 | {
67 | $fake = new CallableFake;
68 |
69 | $fake->assertNotCalled(function (string $arg) {
70 | return $arg === 'a';
71 | });
72 | }
73 |
74 | public function test_assert_not_called_with_false_before_being_called(): void
75 | {
76 | $fake = new CallableFake;
77 |
78 | $fake->assertNotCalled(function (string $arg) {
79 | return $arg === 'x';
80 | });
81 | }
82 |
83 | public function test_assert_not_called_with_true_after_being_called(): void
84 | {
85 | $fake = new CallableFake;
86 | $fake('a');
87 |
88 | try {
89 | $fake->assertNotCalled(function (string $arg) {
90 | return $arg === 'a';
91 | });
92 | $this->fail();
93 | } catch (ExpectationFailedException $e) {
94 | $this->assertStringStartsWith('An unexpected callable was called.', $e->getMessage());
95 | }
96 | }
97 |
98 | public function test_assert_not_called_with_false_after_being_called(): void
99 | {
100 | $fake = new CallableFake;
101 | $fake('a');
102 |
103 | $fake->assertNotCalled(function (string $arg) {
104 | return $arg === 'x';
105 | });
106 | }
107 |
108 | public function test_assert_called_times_with_true_and_expected_count(): void
109 | {
110 | $fake = new CallableFake;
111 | $fake('a');
112 |
113 | $fake->assertCalledTimes(function (string $arg) {
114 | return $arg === 'a';
115 | }, 1);
116 | }
117 |
118 | public function test_assert_called_times_with_true_and_unexpected_count(): void
119 | {
120 | $fake = new CallableFake;
121 | $fake('a');
122 |
123 | try {
124 | $fake->assertCalledTimes(function (string $arg) {
125 | return $arg === 'a';
126 | }, 2);
127 | $this->fail();
128 | } catch (ExpectationFailedException $e) {
129 | $this->assertStringStartsWith('The expected callable was called 1 times instead of the expected 2 times.', $e->getMessage());
130 | }
131 | }
132 |
133 | public function test_assert_called_times_with_false_and_expected_count(): void
134 | {
135 | $fake = new CallableFake;
136 | $fake('a');
137 |
138 | $fake->assertCalledTimes(function (string $arg) {
139 | return $arg === 'x';
140 | }, 0);
141 | }
142 |
143 | public function test_assert_called_times_with_false_and_unexpected_count(): void
144 | {
145 | $fake = new CallableFake;
146 | $fake('a');
147 |
148 | try {
149 | $fake->assertCalledTimes(function (string $arg) {
150 | return $arg === 'x';
151 | }, 2);
152 | $this->fail();
153 | } catch (ExpectationFailedException $e) {
154 | $this->assertStringStartsWith('The expected callable was called 0 times instead of the expected 2 times.', $e->getMessage());
155 | }
156 | }
157 |
158 | public function test_assert_times_invoked_with_unexpected_count(): void
159 | {
160 | $fake = new CallableFake;
161 | $fake();
162 |
163 | try {
164 | $fake->assertTimesInvoked(2);
165 | $this->fail();
166 | } catch (ExpectationFailedException $e) {
167 | $this->assertStringStartsWith('The callable was invoked 1 times instead of the expected 2 times.', $e->getMessage());
168 | }
169 | }
170 |
171 | public function test_assert_times_invoked_with_expected_count(): void
172 | {
173 | $fake = new CallableFake;
174 | $fake();
175 | $fake();
176 |
177 | $fake->assertTimesInvoked(2);
178 | }
179 |
180 | public function test_assert_invoked_when_invoked(): void
181 | {
182 | $fake = new CallableFake;
183 | $fake();
184 |
185 | $fake->assertInvoked();
186 | }
187 |
188 | public function test_assert_invoked_when_not_invoked(): void
189 | {
190 | $fake = new CallableFake;
191 |
192 | try {
193 | $fake->assertInvoked();
194 | $this->fail();
195 | } catch (ExpectationFailedException $e) {
196 | $this->assertStringStartsWith('The callable was not invoked.', $e->getMessage());
197 | }
198 | }
199 |
200 | public function test_assert_not_invoked_when_invoked(): void
201 | {
202 | $fake = new CallableFake;
203 | $fake();
204 |
205 | try {
206 | $fake->assertNotInvoked();
207 | $this->fail();
208 | } catch (ExpectationFailedException $e) {
209 | $this->assertStringStartsWith('The callable was invoked.', $e->getMessage());
210 | }
211 | }
212 |
213 | public function test_assert_not_invoked_when_not_invoked(): void
214 | {
215 | $fake = new CallableFake;
216 |
217 | $fake->assertNotInvoked();
218 | }
219 |
220 | public function test_can_use_as_a_closure(): void
221 | {
222 | $fake = new CallableFake;
223 |
224 | (function (Closure $callback): void {
225 | $callback('a');
226 | })($fake->asClosure());
227 |
228 | $fake->assertCalled(function (string $arg) {
229 | return $arg === 'a';
230 | });
231 | }
232 |
233 | public function test_return_values_can_be_truthy(): void
234 | {
235 | $fake = new CallableFake;
236 | $fake('a');
237 |
238 | $fake->assertCalled(function () {
239 | return 1;
240 | });
241 | }
242 |
243 | public function test_return_values_can_be_falsy(): void
244 | {
245 | $fake = new CallableFake;
246 |
247 | $fake->assertNotCalled(function () {
248 | return 0;
249 | });
250 | }
251 |
252 | public function test_can_specify_invocation_return_types(): void
253 | {
254 | $fake = CallableFake::withReturnResolver(function (int $index): string {
255 | return [
256 | 0 => 'a',
257 | 1 => 'b',
258 | ][$index];
259 | });
260 |
261 | $this->assertSame('a', $fake(0));
262 | $this->assertSame('b', $fake(1));
263 | }
264 |
265 | public function test_null_default_invocation_return_type(): void
266 | {
267 | $fake = new CallableFake;
268 |
269 | $this->assertNull($fake(0));
270 | }
271 |
272 | public function test_was_invoked_when_invoked(): void
273 | {
274 | $callable = new CallableFake;
275 |
276 | $this->assertFalse($callable->wasInvoked());
277 | $this->assertTrue($callable->wasNotInvoked());
278 |
279 | $callable();
280 |
281 | $this->assertTrue($callable->wasInvoked());
282 | $this->assertFalse($callable->wasNotInvoked());
283 | }
284 |
285 | public function test_can_get_invocation_arguments(): void
286 | {
287 | $callable = new CallableFake;
288 | $callable('a', 'b');
289 | $callable('c', 'd');
290 | $callable('x', 'y');
291 |
292 | $invocationArguments = $callable->called(function (string $arg) {
293 | return in_array($arg, ['a', 'c'], true);
294 | });
295 |
296 | $this->assertSame([['a', 'b'], ['c', 'd']], $invocationArguments);
297 | }
298 |
299 | public function test_assert_called_index_with_expected_single_index(): void
300 | {
301 | $callable = new CallableFake;
302 | $callable('b');
303 | $callable('a');
304 | $callable('b');
305 |
306 | $callable->assertCalledIndex(function (string $arg): bool {
307 | return $arg === 'a';
308 | }, 1);
309 | }
310 |
311 | public function test_assert_called_index_with_expected_mutliple_index(): void
312 | {
313 | $callable = new CallableFake;
314 | $callable('b');
315 | $callable('a');
316 | $callable('b');
317 |
318 | $callable->assertCalledIndex(function (string $arg): bool {
319 | return $arg === 'b';
320 | }, [0, 2]);
321 | }
322 |
323 | public function test_assert_called_index_with_unexpected_single_index(): void
324 | {
325 | $callable = new CallableFake;
326 | $callable('b');
327 | $callable('a');
328 | $callable('b');
329 |
330 | try {
331 | $callable->assertCalledIndex(function (string $arg): bool {
332 | return $arg === 'b';
333 | }, 1);
334 | $this->fail();
335 | } catch (ExpectationFailedException $e) {
336 | $this->assertStringStartsWith('The callable was not called in the expected order. Found at index: 0, 2. Expected to be found at index: 1', $e->getMessage());
337 | }
338 | }
339 |
340 | public function test_assert_called_index_with_unexpected_multiple_index(): void
341 | {
342 | $callable = new CallableFake;
343 | $callable('b');
344 | $callable('a');
345 | $callable('b');
346 |
347 | try {
348 | $callable->assertCalledIndex(function (string $arg): bool {
349 | return $arg === 'b';
350 | }, [1, 3]);
351 | $this->fail();
352 | } catch (ExpectationFailedException $e) {
353 | $this->assertStringStartsWith('The callable was not called in the expected order. Found at index: 0, 2. Expected to be found at index: 1, 3', $e->getMessage());
354 | }
355 | }
356 |
357 | public function test_assert_called_index_when_never_called_with_expected_callable(): void
358 | {
359 | $callable = new CallableFake;
360 |
361 | try {
362 | $callable->assertCalledIndex(function (string $arg): bool {
363 | return $arg === 'b';
364 | }, 1);
365 | $this->fail();
366 | } catch (ExpectationFailedException $e) {
367 | $this->assertStringStartsWith('The callable was never called', $e->getMessage());
368 | }
369 | }
370 | }
371 |
--------------------------------------------------------------------------------