Convert tests to Pest

This commit is contained in:
Daniel J. Summers 2024-11-20 20:27:18 -05:00
parent 9327d8fa29
commit c61ff7a831
9 changed files with 2689 additions and 736 deletions

2
.gitignore vendored
View File

@ -1,3 +1,3 @@
.idea .idea
vendor vendor
*.tests.txt *-tests.txt

View File

@ -20,8 +20,8 @@
"php": ">=8.4" "php": ">=8.4"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^11", "phpoption/phpoption": "^1",
"phpoption/phpoption": "^1" "pestphp/pest": "^3.5"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -30,10 +30,15 @@
}, },
"autoload-dev": { "autoload-dev": {
"psr-4": { "psr-4": {
"Test\\": "./tests" "Tests\\": "./tests"
} }
}, },
"archive": { "archive": {
"exclude": [ "/tests", "/.gitattributes", "/.gitignore", "/.git", "/composer.lock" ] "exclude": [ "/tests", "/.gitattributes", "/.gitignore", "/.git", "/composer.lock" ]
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
} }
} }

2207
composer.lock generated

File diff suppressed because it is too large Load Diff

17
phpunit.xml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory suffix=".php">./src</directory>
</include>
</source>
</phpunit>

View File

@ -1,368 +0,0 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace Test;
use BadMethodCallException;
use BitBadger\InspiredByFSharp\Option;
use InvalidArgumentException;
use PhpOption\{None, Some};
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for the Option class
*/
class OptionTest extends TestCase
{
#[TestDox('Value succeeds for Some')]
public function testValueSucceedsForSome(): void
{
$it = Option::Some(9);
$this->assertTrue($it->isSome, 'The option should have been "Some"');
$this->assertEquals(9, $it->value, 'The value was incorrect');
}
#[TestDox('Value fails for None')]
public function testValueFailsForNone(): void
{
$this->expectException(InvalidArgumentException::class);
Option::None()->value;
}
#[TestDox('IsNone succeeds with None')]
public function testIsNoneSucceedsWithNone(): void
{
$this->assertTrue(Option::None()->isNone, '"None" should return true');
}
#[TestDox('IsNone succeeds with Some')]
public function testIsNoneSucceedsWithSome(): void
{
$this->assertFalse(Option::Some(8)->isNone, '"Some" should return false');
}
#[TestDox('IsSome succeeds with None')]
public function testIsSomeSucceedsWithNone(): void
{
$this->assertFalse(Option::None()->isSome, '"None" should return false');
}
#[TestDox('IsSome succeeds with Some')]
public function testIsSomeSucceedsWithSome(): void
{
$this->assertTrue(Option::Some('boo')->isSome, '"Some" should return true');
}
#[TestDox('GetOrDefault succeeds with None')]
public function testGetOrDefaultSucceedsWithNone(): void
{
$this->assertEquals('yes', Option::None()->getOrDefault('yes'), 'Value should have been default');
}
#[TestDox('GetOrDefault succeeds with Some')]
public function testGetOrDefaultSucceedsWithSome(): void
{
$this->assertEquals('no', Option::Some('no')->getOrDefault('yes'), 'Value should have been from option');
}
#[TestDox('GetOrCall succeeds with None')]
public function testGetOrCallSucceedsWithNone(): void
{
$value = Option::None()->getOrCall(new class { public function __invoke(): string { return 'called'; } });
$this->assertEquals('called', $value, 'The value should have been obtained from the callable');
}
#[TestDox('GetOrCall succeeds with Some')]
public function testGetOrCallSucceedsWithSome(): void
{
$value = Option::Some('passed')->getOrCall(
new class { public function __invoke(): string { return 'called'; } });
$this->assertEquals('passed', $value, 'The value should have been obtained from the option');
}
#[TestDox('GetOrThrow succeeds with Some')]
public function testGetOrThrowSucceedsWithSome(): void
{
$value = Option::Some('no throw')->getOrThrow(fn() => new BadMethodCallException('oops'));
$this->assertEquals('no throw', $value, 'The "Some" value should have been returned');
}
#[TestDox('GetOrThrow succeeds with None')]
public function testGetOrThrowSucceedsWithNone(): void
{
$this->expectException(BadMethodCallException::class);
Option::None()->getOrThrow(fn() => new BadMethodCallException('oops'));
}
#[TestDox('Bind succeeds with None')]
public function testBindSucceedsWithNone(): void
{
$original = Option::None();
$bound = $original->bind(fn($it) => Option::Some('value'));
$this->assertTrue($bound->isNone, 'The option should have been None');
$this->assertSame($original, $bound, 'The same None instance should have been returned');
}
#[TestDox('Bind succeeds with Some and Some')]
public function testBindSucceedsWithSomeAndSome(): void
{
$bound = Option::Some('hello')->bind(fn($it) => Option::Some('goodbye'));
$this->assertTrue($bound->isSome, 'The option should have been Some');
$this->assertEquals('goodbye', $bound->value, 'The bound function was not called');
}
#[TestDox('Bind succeeds with Some and None')]
public function testBindSucceedsWithSomeAndNone(): void
{
$bound = Option::Some('greetings')->bind(fn($it) => Option::None());
$this->assertTrue($bound->isNone, 'The option should have been None');
}
#[TestDox('Contains succeeds with None')]
public function testContainsSucceedsWithNone(): void
{
$this->assertFalse(Option::None()->contains(null), '"None" should always return false');
}
#[TestDox('Contains succeeds with Some when strictly equal')]
public function testContainsSucceedsWithSomeWhenStrictlyEqual(): void
{
$this->assertTrue(Option::Some(3)->contains(3), '"Some" with strict equality should be true');
}
#[TestDox('Contains succeeds with Some when strictly unequal')]
public function testContainsSucceedsWithSomeWhenStrictlyUnequal(): void
{
$this->assertFalse(Option::Some('3')->contains(3), '"Some" with strict equality should be false');
}
#[TestDox('Contains succeeds with Some when loosely equal')]
public function testContainsSucceedsWithSomeWhenLooselyEqual(): void
{
$this->assertTrue(Option::Some('3')->contains(3, strict: false), '"Some" with loose equality should be true');
}
#[TestDox('Contains succeeds with Some when loosely unequal')]
public function testContainsSucceedsWithSomeWhenLooselyUnequal(): void
{
$this->assertFalse(Option::Some('3')->contains(4, strict: false), '"Some" with loose equality should be false');
}
#[TestDox('Exists succeeds with Some when matching')]
public function testExistsSucceedsWithSomeWhenMatching(): void
{
$this->assertTrue(Option::Some(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
}
#[TestDox('Exists succeeds with Some when not matching')]
public function testExistsSucceedsWithSomeWhenNotMatching(): void
{
$this->assertFalse(Option::Some(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
}
#[TestDox('Exists succeeds with None')]
public function testExistsSucceedsWithNone(): void
{
$this->assertFalse(Option::None()->exists(fn($it) => true), 'Exists should have returned false');
}
#[TestDox('Map succeeds with None')]
public function testMapSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$mapped = $none->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isNone, 'The mapped option should be "None"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($none, $mapped, 'The same "None" instance should have been returned');
}
#[TestDox('Map succeeds with Some')]
public function testMapSucceedsWithSome(): void
{
$mapped = Option::Some('abc ')->map(fn($it) => str_repeat($it, 2));
$this->assertTrue($mapped->isSome, 'The mapped option should be "Some"');
$this->assertEquals('abc abc ', $mapped->value, 'The mapping function was not called correctly');
}
#[TestDox('Map fails with Some when mapping is null')]
public function testMapFailsWithSomeWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Option::Some('oof')->map(fn($it) => null);
}
#[TestDox('Iter succeeds with None')]
public function testIterSucceedsWithNone(): void
{
$target = new class { public mixed $called = null; };
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('Iter succeeds with Some')]
public function testIterSucceedsWithSome(): void
{
$target = new class { public mixed $called = null; };
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
$this->assertEquals(33, $target->called, 'The function should have been called with the "Some" value');
}
#[TestDox('Filter succeeds with None')]
public function testFilterSucceedsWithNone(): void
{
$tattle = new class { public bool $called = false; };
$none = Option::None();
$filtered = $none->filter(function () use ($tattle)
{
$tattle->called = true;
return true;
});
$this->assertTrue($filtered->isNone, 'The filtered option should have been "None"');
$this->assertFalse($tattle->called, 'The callable should not have been called');
$this->assertSame($none, $filtered, 'The "None" instance returned should have been the one passed');
}
#[TestDox('Filter succeeds with Some when true')]
public function testFilterSucceedsWithSomeWhenTrue(): void
{
$some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isSome, 'The filtered option should have been "Some"');
$this->assertEquals(12, $filtered->value, 'The filtered option value is incorrect');
$this->assertSame($some, $filtered, 'The same "Some" instance should have been returned');
}
#[TestDox('Filter succeeds with Some when false')]
public function testFilterSucceedsWithSomeWhenFalse(): void
{
$some = Option::Some(23);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
$this->assertTrue($filtered->isNone, 'The filtered option should have been "None"');
}
#[TestDox('Unwrap succeeds with None')]
public function testUnwrapSucceedsWithNone(): void
{
$this->assertNull(Option::None()->unwrap(), '"None" should return null');
}
#[TestDox('Unwrap succeeds with Some')]
public function testUnwrapSucceedsWithSome(): void
{
$this->assertEquals('boy howdy', Option::Some('boy howdy')->unwrap(), '"Some" should return its value');
}
#[TestDox('Tap succeeds with Some')]
public function testTapSucceedsWithSome(): void
{
$value = '';
$original = Option::Some('testing');
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
$this->assertEquals('testing', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
}
#[TestDox('Tap succeeds with None')]
public function testTapSucceedsWithNone(): void
{
$value = '';
$original = Option::None();
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
$this->assertEquals('none', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same option should have been returned');
}
#[TestDox('ToArray succeeds with Some')]
public function testToArraySucceedsWithSome(): void
{
$arr = Option::Some('15')->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEquals(['15'], $arr, 'The array was not created correctly');
}
#[TestDox('ToArray succeeds with None')]
public function testToArraySucceedsWithNone(): void
{
$arr = Option::None()->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEmpty($arr, 'The array should have been empty');
}
#[TestDox('ToPhpOption succeeds for Some')]
public function testToPhpOptionSucceedsForSome(): void
{
$opt = Option::Some('php')->toPhpOption();
$this->assertNotNull($opt, 'The PhpOption should not have been null');
$this->assertInstanceOf('PhpOption\Some', $opt, 'The PhpOption should have been "Some"');
$this->assertTrue($opt->isDefined(), 'There should have been a value for the PhpOption');
$this->assertEquals('php', $opt->get(), 'The value was not correct');
}
#[TestDox('ToPhpOption succeeds for None')]
public function testToPhpOptionSucceedsForNone(): void
{
$opt = Option::None()->toPhpOption();
$this->assertNotNull($opt, 'The PhpOption should not have been null');
$this->assertInstanceOf('PhpOption\None', $opt, 'The PhpOption should have been "None"');
$this->assertFalse($opt->isDefined(), 'There should not have been a value for the PhpOption');
}
public function testSomeSucceedsWithValue(): void
{
$it = Option::Some('hello');
$this->assertTrue($it->isSome, 'The option should have been "Some"');
}
public function testSomeFailsWithNull(): void
{
$this->expectException(InvalidArgumentException::class);
Option::Some(null);
}
public function testNoneSucceeds(): void
{
$it = Option::None();
$this->assertTrue($it->isNone, 'The option should have been "None"');
}
public function testOfSucceedsWithNull(): void
{
$it = Option::of(null);
$this->assertTrue($it->isNone, '"null" should have created a "None" option');
}
public function testOfSucceedsWithNonNull(): void
{
$it = Option::of('test');
$this->assertTrue($it->isSome, 'A non-null value should have created a "Some" option');
$this->assertEquals('test', $it->value, 'The value was not assigned correctly');
}
#[TestDox('Of succeeds with PhpOption\Some')]
public function testOfSucceedsWithPhpOptionSome(): void
{
$it = Option::of(Some::create('something'));
$this->assertTrue($it->isSome, 'A "Some" PhpOption should have created a "Some" option');
$this->assertEquals('something', $it->value, 'The value was not assigned correctly');
}
#[TestDox('Of succeeds with PhpOption\None')]
public function testOfSucceedsWithPhpOptionNone(): void
{
$it = Option::of(None::create());
$this->assertTrue($it->isNone, 'A "None" PhpOption should have created a "None" option');
}
}

45
tests/Pest.php Normal file
View File

@ -0,0 +1,45 @@
<?php
/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "pest()" function to bind a different classes or traits.
|
*/
// pest()->extend(Tests\TestCase::class)->in('Feature');
/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
expect()->extend('toBeOne', function () {
return $this->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
function something()
{
// ..
}

View File

@ -1,313 +0,0 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
namespace Test;
use BitBadger\InspiredByFSharp\Result;
use InvalidArgumentException;
use PHPUnit\Framework\Attributes\TestDox;
use PHPUnit\Framework\TestCase;
/**
* Unit tests for the Result class
*/
class ResultTest extends TestCase
{
#[TestDox('OK property succeeds for OK result')]
public function testOKPropertySucceedsForOKResult(): void
{
$result = Result::OK('yay');
$this->assertEquals('yay', $result->ok, 'The OK result should have been returned');
}
#[TestDox('OK property fails for Error result')]
public function testOKPropertyFailsForErrorResult(): void
{
$this->expectException(InvalidArgumentException::class);
Result::Error('whoops')->ok;
}
#[TestDox('Error property succeeds for Error result')]
public function testErrorPropertySucceedsForErrorResult(): void
{
$result = Result::Error('boo');
$this->assertEquals('boo', $result->error, 'The Error result should have been returned');
}
#[TestDox('Error property fails for OK result')]
public function testErrorPropertyFailsForOKResult(): void
{
$this->expectException(InvalidArgumentException::class);
Result::OK('yeah')->error;
}
#[TestDox('IsOK succeeds for OK result')]
public function testIsOKSucceedsForOKResult(): void
{
$result = Result::OK('ok');
$this->assertTrue($result->isOK, 'The check for "OK" should have returned true');
}
#[TestDox('IsOK succeeds for Error result')]
public function testIsOKSucceedsForErrorResult(): void
{
$result = Result::Error('error');
$this->assertFalse($result->isOK, 'The check for "OK" should have returned false');
}
#[TestDox('IsError succeeds for Error result')]
public function testIsErrorSucceedsForErrorResult(): void
{
$result = Result::Error('not ok');
$this->assertTrue($result->isError, 'The check for "Error" should have returned true');
}
#[TestDox('IsError succeeds for OK result')]
public function testIsErrorSucceedsForOKResult(): void
{
$result = Result::OK('fine');
$this->assertFalse($result->isError, 'The check for "Error" should have returned false');
}
#[TestDox('Bind succeeds for OK with OK')]
public function testBindSucceedsForOKWithOK(): void
{
$result = Result::OK('one')->bind(fn($it) => Result::OK("$it two"));
$this->assertTrue($result->isOK, 'The result should have been OK');
$this->assertEquals('one two', $result->ok, 'The bound function was not called');
}
#[TestDox('Bind succeeds for OK with Error')]
public function testBindSucceedsForOKWithError(): void
{
$result = Result::OK('three')->bind(fn($it) => Result::Error('back to two'));
$this->assertTrue($result->isError, 'The result should have been Error');
$this->assertEquals('back to two', $result->error, 'The bound function was not called');
}
#[TestDox('Bind succeeds for Error')]
public function testBindSucceedsForError(): void
{
$original = Result::Error('oops');
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
$this->assertTrue($result->isError, 'The result should have been Error');
$this->assertSame($original, $result, 'The same Error result should have been returned');
}
#[TestDox('Contains succeeds for Error result')]
public function testContainsSucceedsForErrorResult(): void
{
$this->assertFalse(Result::Error('ouch')->contains('ouch'), '"Error" should always return false');
}
#[TestDox('Contains succeeds for OK result when strictly equal')]
public function testContainsSucceedsForOKResultWhenStrictlyEqual(): void
{
$this->assertTrue(Result::OK(18)->contains(18), '"OK" with strict equality should be true');
}
#[TestDox('Contains succeeds for OK result when strictly unequal')]
public function testContainsSucceedsForOKResultWhenStrictlyUnequal(): void
{
$this->assertFalse(Result::OK(18)->contains('18'), '"OK" with strict equality should be false');
}
#[TestDox('Contains succeeds for OK result when loosely equal')]
public function testContainsSucceedsForOKResultWhenLooselyEqual(): void
{
$this->assertTrue(Result::OK(18)->contains('18', strict: false), '"OK" with loose equality should be true');
}
#[TestDox('Contains succeeds for OK result when loosely unequal')]
public function testContainsSucceedsForOKResultWhenLooselyUnequal(): void
{
$this->assertFalse(Result::OK(18)->contains(17, strict: false), '"OK" with loose equality should be false');
}
#[TestDox('Exists succeeds for OK result when matching')]
public function testExistsSucceedsForOKResultWhenMatching(): void
{
$this->assertTrue(Result::OK(14)->exists(fn($it) => $it < 100), 'Exists should have returned true');
}
#[TestDox('Exists succeeds for OK result when not matching')]
public function testExistsSucceedsForOKResultWhenNotMatching(): void
{
$this->assertFalse(Result::OK(14)->exists(fn($it) => $it > 100), 'Exists should have returned false');
}
#[TestDox('Exists succeeds for Error result')]
public function testExistsSucceedsForErrorResult(): void
{
$this->assertFalse(Result::Error(true)->exists(fn($it) => true), 'Exists should have returned false');
}
#[TestDox('Map succeeds for OK result')]
public function testMapSucceedsForOKResult(): void
{
$ok = Result::OK('yard');
$mapped = $ok->map(fn($it) => strrev($it));
$this->assertTrue($mapped->isOK, 'The mapped result should be "OK"');
$this->assertEquals('dray', $mapped->ok, 'The mapping function was not called correctly');
}
#[TestDox('Map fails for OK result when mapping is null')]
public function testMapFailsForOKResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::OK('not null')->map(fn($it) => null);
}
#[TestDox('Map succeeds for Error result')]
public function testMapSucceedsForErrorResult(): void
{
$tattle = new class { public bool $called = false; };
$error = Result::Error('nope');
$mapped = $error->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isError, 'The mapped result should be "Error"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($error, $mapped, 'The same "Error" instance should have been returned');
}
#[TestDox('MapError succeeds for OK result')]
public function testMapErrorSucceedsForOKResult(): void
{
$tattle = new class { public bool $called = false; };
$ok = Result::OK('sure');
$mapped = $ok->mapError(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
$this->assertTrue($mapped->isOK, 'The mapped result should be "OK"');
$this->assertFalse($tattle->called, 'The mapping function should not have been called');
$this->assertSame($ok, $mapped, 'The same "OK" instance should have been returned');
}
#[TestDox('MapError succeeds for Error result')]
public function testMapErrorSucceedsForErrorResult(): void
{
$error = Result::Error('taco');
$mapped = $error->mapError(fn($it) => str_repeat('*', strlen($it)));
$this->assertTrue($mapped->isError, 'The mapped result should be "Error"');
$this->assertEquals('****', $mapped->error, 'The mapping function was not called correctly');
}
#[TestDox('MapError fails for Error result when mapping is null')]
public function testMapErrorFailsForErrorResultWhenMappingIsNull(): void
{
$this->expectException(InvalidArgumentException::class);
Result::Error('pizza')->mapError(fn($it) => null);
}
#[TestDox('Iter succeeds for OK result')]
public function testIterSucceedsForOKResult(): void
{
$target = new class { public mixed $called = null; };
Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
$this->assertEquals(77, $target->called, 'The function should have been called with the "OK" value');
}
#[TestDox('Iter succeeds for Error result')]
public function testIterSucceedsForErrorResult(): void
{
$target = new class { public mixed $called = null; };
Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
$this->assertNull($target->called, 'The function should not have been called');
}
#[TestDox('ToArray succeeds for OK result')]
public function testToArraySucceedsForOKResult(): void
{
$arr = Result::OK('yay')->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEquals(['yay'], $arr, 'The array was not created correctly');
}
#[TestDox('ToArray succeeds for Error result')]
public function testToArraySucceedsForErrorResult(): void
{
$arr = Result::Error('oh no')->toArray();
$this->assertNotNull($arr, 'The array should not have been null');
$this->assertEmpty($arr, 'The array should have been empty');
}
#[TestDox('ToOption succeeds for OK result')]
public function testToOptionSucceedsForOKResult()
{
$value = Result::OK(99)->toOption();
$this->assertTrue($value->isSome, 'An "OK" result should map to a "Some" option');
$this->assertEquals(99, $value->value, 'The value is not correct');
}
#[TestDox('ToOption succeeds for Error result')]
public function testToOptionSucceedsForErrorResult()
{
$value = Result::Error('file not found')->toOption();
$this->assertTrue($value->isNone, 'An "Error" result should map to a "None" option');
}
#[TestDox('Tap succeeds for OK result')]
public function testTapSucceedsForOKResult(): void
{
$value = '';
$original = Result::OK('working');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
});
$this->assertEquals('OK: working', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
#[TestDox('Tap succeeds for Error result')]
public function testTapSucceedsForErrorResult(): void
{
$value = '';
$original = Result::Error('failed');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
});
$this->assertEquals('Error: failed', $value, 'The tapped function was not called');
$this->assertSame($original, $tapped, 'The same result should have been returned');
}
#[TestDox('OK succeeds for non null result')]
public function testOKSucceedsForNonNullResult(): void
{
$result = Result::OK('something');
$this->assertTrue($result->isOK, 'The result should have been "OK"');
$this->assertEquals('something', $result->ok, 'The "OK" value was incorrect');
}
#[TestDox('OK fails for null result')]
public function testOKFailsForNullResult(): void
{
$this->expectException(InvalidArgumentException::class);
Result::OK(null);
}
#[TestDox('Error succeeds for non null result')]
public function testErrorSucceedsForNonNullResult(): void
{
$result = Result::Error('sad trombone');
$this->assertTrue($result->isError, 'The result should have been "Error"');
$this->assertEquals('sad trombone', $result->error, 'The "Error" value was incorrect');
}
#[TestDox('Error fails for null result')]
public function testErrorFailsForNullResult(): void
{
$this->expectException(InvalidArgumentException::class);
Result::Error(null);
}
}

256
tests/Unit/OptionTest.php Normal file
View File

@ -0,0 +1,256 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\Option;
use PhpOption\{None, Some};
describe('->value', function () {
test('retrieves the value for Some', function () {
expect(Option::Some(9)->value)->toBe(9);
});
test('throws an exception for None', function () {
expect(fn() => Option::None()->value)->toThrow(InvalidArgumentException::class);
});
});
describe('->isNone', function () {
test('returns true for None', function () {
expect(Option::None()->isNone)->toBeTrue();
});
test('returns false for Some', function () {
expect(Option::Some(8)->isNone)->toBeFalse();
});
});
describe('->isSome', function () {
test('returns false for None', function () {
expect(Option::None()->isSome)->toBeFalse();
});
test('returns true for Some', function () {
expect(Option::Some('boo')->isSome)->toBeTrue();
});
});
describe('->getOrDefault()', function () {
test('returns default value for None', function () {
expect(Option::None()->getOrDefault('yes'))->toBe('yes');
});
test('returns option value for Some', function () {
expect(Option::Some('no')->getOrDefault('yes'))->toBe('no');
});
});
describe('->getOrCall()', function () {
test('returns value from callable for None', function () {
expect(Option::None()->getOrCall(fn() => 'called'))->toBe('called');
});
test('returns option value for Some', function () {
expect(Option::Some('passed')->getOrCall(fn() => 'called'))->toBe('passed');
});
});
describe('->getOrThrow()', function () {
test('throws an exception for None', function () {
expect(fn() => Option::None()->getOrThrow(fn() => throw new BadMethodCallException()))
->toThrow(BadMethodCallException::class);
});
test('returns option value for Some', function () {
expect(Option::Some('no throw')->getOrThrow(fn() => throw new BadMethodCallException()))->toBe('no throw');
});
});
describe('->bind()', function () {
test('returns None when binding against None', function () {
$original = Option::None();
$bound = $original->bind(fn($it) => Option::Some('value'));
expect($bound)->isNone->toBeTrue()->and($bound)->toBe($original);
});
test('returns Some when binding against Some with Some', function () {
expect(Option::Some('hello')->bind(fn($it) => Option::Some('goodbye')))
->isSome->toBeTrue()->value->toBe('goodbye');
});
test('returns None when binding against Some with None', function () {
expect(Option::Some('greetings')->bind(fn($it) => Option::None()))->isNone->toBeTrue();
});
});
describe('->contains()', function () {
test('returns false for None', function () {
expect(Option::None()->contains(null))->toBeFalse();
});
test('returns true for Some when strict equality is matched', function () {
expect(Option::Some(3)->contains(3))->toBeTrue();
});
test('returns false for Some when strict equality is not matched', function () {
expect(Option::Some('3')->contains(3))->toBeFalse();
});
test('returns true for Some when loose equality is matched', function () {
expect(Option::Some('3')->contains(3, strict: false))->toBeTrue();
});
test('returns false for Some when loose equality is not matched', function () {
expect(Option::Some('3')->contains(4, strict: false))->toBeFalse();
});
});
describe('->exists()', function () {
test('returns false for None', function () {
expect(Option::None()->exists(fn($it) => true))->toBeFalse();
});
test('returns true for Some and matching condition', function () {
expect(Option::Some(14)->exists(fn($it) => $it < 100))->toBeTrue();
});
test('returns false for Some and non-matching condition', function () {
expect(Option::Some(14)->exists(fn($it) => $it > 100))->toBeFalse();
});
});
describe('->map()', function () {
test('does nothing for None', function () {
$tattle = new class { public bool $called = false; };
$none = Option::None();
$mapped = $none->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
expect($mapped)
->isNone->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($none);
});
test('maps value for Some', function () {
expect(Option::Some('abc ')->map(fn($it) => str_repeat($it, 2)))
->isSome->toBeTrue()->value->toBe('abc abc ');
});
test('throws an exception if mapping returns null', function () {
expect(fn() => Option::Some('oof')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
});
});
describe('->iter()', function () {
test('does nothing for None', function () {
$target = new class { public mixed $called = null; };
Option::None()->iter(function () use ($target) { $target->called = 'uh oh'; });
expect($target->called)->toBeNull();
});
test('iterates for Some', function () {
$target = new class { public mixed $called = null; };
Option::Some(33)->iter(function ($it) use ($target) { $target->called = $it; });
expect($target->called)->toBe(33);
});
});
describe('->filter()', function () {
test('does nothing for None', function () {
$tattle = new class { public bool $called = false; };
$none = Option::None();
$filtered = $none->filter(function () use ($tattle)
{
$tattle->called = true;
return true;
});
expect($filtered)
->isNone->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($filtered)->toBe($none);
});
test('returns Some when filter is matched', function () {
$some = Option::Some(12);
$filtered = $some->filter(fn($it) => $it % 2 === 0);
expect($filtered)
->isSome->toBeTrue()
->value->toBe(12)
->and($filtered)->toBe($some);
});
test('returns None when filter is not matched', function () {
expect(Option::Some(23)->filter(fn($it) => $it % 2 === 0)->isNone)->toBeTrue();
});
});
describe('->unwrap()', function () {
test('returns null for None', function () {
expect(Option::None()->unwrap())->toBeNull();
});
test('returns option value for Some', function () {
expect(Option::Some('boy howdy')->unwrap())->toBe('boy howdy');
});
});
describe('->tap()', function () {
test('is called for None', function () {
$value = '';
$original = Option::None();
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
expect($value)->toBe('none')->and($original)->toBe($tapped);
});
test('is called for Some', function () {
$value = '';
$original = Option::Some('testing');
$tapped = $original->tap(
function (Option $it) use (&$value) { $value = $it->isSome ? $it->value : 'none'; });
expect($value)->toBe('testing')->and($original)->toBe($tapped);
});
});
describe('->toArray()', function () {
test('returns empty array for None', function () {
expect(Option::None()->toArray())->not->toBeNull()->toBeEmpty();
});
test('returns one-item array for Some', function () {
expect(Option::Some('15')->toArray())->not->toBeNull()->toBe(['15']);
});
});
describe('->toPhpOption()', function () {
test('converts None', function () {
expect(Option::None()->toPhpOption())
->toBeInstanceOf('PhpOption\None')
->not->toBeNull()
->isDefined()->toBeFalse();
});
test('converts Some', function () {
expect(Option::Some('php')->toPhpOption())
->toBeInstanceOf('PhpOption\Some')
->not->toBeNull()
->isDefined()->toBeTrue()
->get()->toBe('php');
});
});
describe('::Some()', function () {
test('creates a Some option when given a value', function () {
expect(Option::Some('hello'))->not->toBeNull()->isSome->toBeTrue();
});
test('throws an exception when given null', function () {
expect(fn() => Option::Some(null))->toThrow(InvalidArgumentException::class);
});
});
describe('::None()', function () {
test('creates a None option', function () {
expect(Option::None())->not->toBeNull()->isNone->toBeTrue();
});
});
describe('::of()', function () {
test('creates a None option when given null', function () {
expect(Option::of(null))->not->toBeNull()->isNone->toBeTrue();
});
test('creates a Some option when given a value', function () {
expect(Option::of('test'))->not->toBeNull()
->isSome->toBeTrue()
->value->toBe('test');
});
test('creates a None option when given PhpOption\None', function () {
expect(Option::of(None::create()))->isNone->toBeTrue();
});
test('creates a Some option when given PhpOption\Some', function () {
expect(Option::of(Some::create('something')))->isSome->toBeTrue()->value->toBe('something');
});
});

206
tests/Unit/ResultTest.php Normal file
View File

@ -0,0 +1,206 @@
<?php
/**
* @author Daniel J. Summers <daniel@bitbadger.solutions>
* @license MIT
*/
declare(strict_types=1);
use BitBadger\InspiredByFSharp\{Option, Result};
describe('->ok', function () {
test('returns OK value for OK result', function () {
expect(Result::OK('yay')->ok)->toBe('yay');
});
test('throws an exception for Error result', function () {
expect(fn() => Result::Error('whoops')->ok)->toThrow(InvalidArgumentException::class);
});
});
describe('->error', function () {
test('throws an exception for OK result', function () {
expect(fn() => Result::OK('yeah')->error)->toThrow(InvalidArgumentException::class);
});
test('returns Error value for Error result', function () {
expect(Result::Error('boo')->error)->toBe('boo');
});
});
describe('->isOK', function () {
test('returns true for OK result', function () {
expect(Result::OK('ok')->isOK)->toBeTrue();
});
test('returns false for Error result', function () {
expect(Result::Error('error')->isOK)->toBeFalse();
});
});
describe('->isError', function () {
test('returns false for OK result', function () {
expect(Result::OK('fine')->isError)->toBeFalse();
});
test('returns true for Error result', function () {
expect(Result::Error('not ok')->isError)->toBeTrue();
});
});
describe('->bind()', function () {
test('returns OK when binding against OK with OK', function () {
expect(Result::OK('one')->bind(fn($it) => Result::OK("$it two")))
->isOK->toBeTrue()->ok->toBe('one two');
});
test('returns Error when binding against OK with Error', function () {
expect(Result::OK('three')->bind(fn($it) => Result::Error('back to two')))
->isError->toBeTrue()->error->toBe('back to two');
});
test('returns Error when binding against Error', function () {
$original = Result::Error('oops');
$result = $original->bind(fn($it) => Result::OK('never mind - it worked!'));
expect($result->isError)->toBeTrue()
->and($result)->toBe($original);
});
});
describe('->contains()', function () {
test('returns true when OK is strictly equal', function () {
expect(Result::OK(18)->contains(18))->toBeTrue();
});
test('returns false when OK is not strictly equal', function () {
expect(Result::OK(18)->contains('18'))->toBeFalse();
});
test('returns true when OK is loosely equal', function () {
expect(Result::OK(18)->contains('18', strict: false))->toBeTrue();
});
test('returns false when OK is not loosely equal', function () {
expect(Result::OK(18)->contains(17, strict: false))->toBeFalse();
});
test('returns false for Error', function () {
expect(Result::Error('ouch')->contains('ouch'))->toBeFalse();
});
});
describe('->exists()', function () {
test('returns true for OK when condition matches', function () {
expect(Result::OK(14)->exists(fn($it) => $it < 100))->toBeTrue();
});
test('returns false for OK when condition does not match', function () {
expect(Result::OK(14)->exists(fn($it) => $it > 100))->toBeFalse();
});
test('returns false for Error', function () {
expect(Result::Error(true)->exists(fn($it) => true))->toBeFalse();
});
});
describe('->map()', function () {
test('maps value for OK', function () {
expect(Result::OK('yard')->map(fn($it) => strrev($it)))
->isOK->toBeTrue()->ok->toBe('dray');
});
test('throws an exception for OK when mapping result is null', function () {
expect(fn() => Result::OK('not null')->map(fn($it) => null))->toThrow(InvalidArgumentException::class);
});
test('does nothing for Error', function () {
$tattle = new class { public bool $called = false; };
$error = Result::Error('nope');
$mapped = $error->map(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
expect($mapped)
->isError->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($error);
});
});
describe('->mapError()', function () {
test('does nothing for OK', function () {
$tattle = new class { public bool $called = false; };
$ok = Result::OK('sure');
$mapped = $ok->mapError(function () use ($tattle)
{
$tattle->called = true;
return 'hello';
});
expect($mapped)
->isOK->toBeTrue()
->and($tattle->called)->toBeFalse()
->and($mapped)->toBe($ok);
});
test('maps value for Error', function () {
expect(Result::Error('taco')->mapError(fn($it) => str_repeat('*', strlen($it))))
->isError->toBeTrue()->error->toBe('****');
});
test('throws an exception for Error when mapping result is null', function () {
expect(fn() => Result::Error('pizza')->mapError(fn($it) => null))->toThrow(InvalidArgumentException::class);
});
});
describe('->iter()', function () {
test('iterates for OK', function () {
$target = new class { public mixed $called = null; };
Result::OK(77)->iter(function ($it) use ($target) { $target->called = $it; });
expect($target->called)->toBe(77);
});
test('does nothing for Error', function () {
$target = new class { public mixed $called = null; };
Result::Error('')->iter(function () use ($target) { $target->called = 'uh oh'; });
expect($target->called)->toBeNull();
});
});
describe('->toArray()', function () {
test('returns a one-item array for OK', function () {
expect(Result::OK('yay')->toArray())->not->toBeNull()->toBe(['yay']);
});
test('returns an empty array for Error', function () {
expect(Result::Error('oh no')->toArray())->not->toBeNull()->toBeEmpty();
});
});
describe('->toOption()', function () {
test('returns a Some option for OK', function () {
expect(Result::OK(99)->toOption())->isSome->toBeTrue()->value->toBe(99);
});
test('returns a None option for Error', function () {
expect(Result::Error('file not found')->toOption())->isNone->toBeTrue();
});
});
describe('->tap()', function () {
test('is called for OK', function () {
$value = '';
$original = Result::OK('working');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
});
expect($value)->toBe('OK: working')->and($tapped)->toBe($original);
});
test('is called for Error', function () {
$value = '';
$original = Result::Error('failed');
$tapped = $original->tap(function (Result $it) use (&$value) {
$value = $it->isOK ? 'OK: ' . $it->ok : 'Error: ' . $it->error;
});
expect($value)->toBe('Error: failed')->and($tapped)->toBe($original);
});
});
describe('::OK()', function () {
test('creates an OK result for a non-null value', function () {
expect(Result::OK('something'))->isOK->toBeTrue()->ok->toBe('something');
});
test('throws an exception for OK with a null value', function () {
expect(fn() => Result::OK(null))->toThrow(InvalidArgumentException::class);
});
});
describe('::Error()', function () {
test('creates an Error result for a non-null value', function () {
expect(Result::Error('sad trombone'))->isError->toBeTrue()->error->toBe('sad trombone');
});
test('throws an exception for Error with a null value', function () {
expect(fn() => Result::Error(null))->toThrow(InvalidArgumentException::class);
});
});