* @license MIT */ declare(strict_types=1); namespace Test; 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('Get succeeds for Some')] public function testGetSucceedsForSome(): void { $it = Option::Some(9); $this->assertTrue($it->isSome(), 'The option should have been "Some"'); $this->assertEquals(9, $it->get(), 'The value was incorrect'); } #[TestDox('Get fails for None')] public function testGetFailsForNone(): void { $this->expectException(InvalidArgumentException::class); Option::None()->get(); } #[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('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->get(), '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->get(), '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('Is succeeds with None')] public function testIsSucceedsWithNone(): void { $this->assertFalse(Option::None()->is(null), '"None" should always return false'); } #[TestDox('Is succeeds with Some when strictly equal')] public function testIsSucceedsWithSomeWhenStrictlyEqual(): void { $this->assertTrue(Option::Some(3)->is(3), '"Some" with strict equality should be true'); } #[TestDox('Is succeeds with Some when strictly unequal')] public function testIsSucceedsWithSomeWhenStrictlyUnequal(): void { $this->assertFalse(Option::Some('3')->is(3), '"Some" with strict equality should be false'); } #[TestDox('Is succeeds with Some when loosely equal')] public function testIsSucceedsWithSomeWhenLooselyEqual(): void { $this->assertTrue(Option::Some('3')->is(3, strict: false), '"Some" with loose equality should be true'); } #[TestDox('Is succeeds with Some when loosely unequal')] public function testIsSucceedsWithSomeWhenLooselyUnequal(): void { $this->assertFalse(Option::Some('3')->is(4, strict: false), '"Some" with loose equality should be false'); } #[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->get() : '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->get() : 'none'; }); $this->assertEquals('none', $value, 'The tapped function was not called'); $this->assertSame($original, $tapped, 'The same option should have been returned'); } #[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->get(), '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->get(), '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'); } }