diff --git a/src/Expectation.php b/src/Expectation.php index ebfd6302..11c04542 100644 --- a/src/Expectation.php +++ b/src/Expectation.php @@ -32,6 +32,7 @@ use Pest\Support\ExpectationPipeline; use Pest\Support\Reflection; use PHPUnit\Architecture\Elements\ObjectDescription; +use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\ExpectationFailedException; use ReflectionEnum; use ReflectionMethod; @@ -324,6 +325,35 @@ public function when(callable|bool $condition, callable $callback): self return $this; } + public function toBeOneOf(Closure ...$tests): self + { + if ($tests === []) { + return $this; + } + + $matches = []; + $exceptions = []; + + foreach ($tests as $key => $test) { + try { + $test(new Expectation($this->value)); + $matches[] = $key; + } catch (AssertionFailedError) { + $exceptions[] = $key; + } + } + + if (count($matches) === 1) { + return $this; + } + + if (count($matches) > 1) { + throw new ExpectationFailedException('Failed asserting value matches exactly one expectation (matches: '.implode(', ', $matches).').'); + } + + throw new ExpectationFailedException('Failed asserting value matches any expectations.'); + } + /** * Dynamically calls methods on the class or creates a new higher order expectation. * diff --git a/tests/Features/Expect/toBeOneOf.php b/tests/Features/Expect/toBeOneOf.php new file mode 100644 index 00000000..071b35ce --- /dev/null +++ b/tests/Features/Expect/toBeOneOf.php @@ -0,0 +1,40 @@ +toBeTrue()->and(false)->toBeFalse(); + +test('risky with no assertions', function () { + expect(1)->toBeOneOf(); +})->throwsNoExceptions(); + +test('to be one of', function () { + expect(1)->toBeOneOf(fn ($e) => $e->toBe(1), fn ($e) => $e->toBe(2), fn ($e) => $e->toBe(3)); +}); + +test('it does not short-circuit', function () { + $executed = 0; + expect(1)->toBeOneOf(function ($e) use (&$executed) { + $executed++; + + return $e->toBe(1); + }, function ($e) use (&$executed) { + $executed++; + + return $e->toBe(2); + }, function ($e) use (&$executed) { + $executed++; + + return $e->toBe(3); + }); + + expect($executed)->toBe(3); +}); + +test('failure with multiple matches', function () { + expect(1)->toBeOneOf(fn ($e) => $e->toBe(1), fn ($e) => $e->toBe(1)); +})->throws(ExpectationFailedException::class, 'Failed asserting value matches exactly one expectation (matches: 0, 1).'); + +test('failure with no matches', function () { + expect(1)->toBeOneOf(fn ($e) => $e->toBe(2), fn ($e) => $e->toBe(2)); +})->throws(ExpectationFailedException::class, 'Failed asserting value matches any expectations.');