Scope of Change
This RFC suggests to create a new testing library for the XP Framework. It will only work with baseless, single-instance test classes.
Rationale
- Drop support for
extends TestCase, supporting of which costs lots of code
- Drop support for seldomly used features such as test timeouts, exotic
Values variations or external provider methods
- Simplify test actions, these should have not use exceptions for flow control
- Integrate
Assert::that() extended assertions to simplify commonly used test code
Functionality
In a nutshell, tests reside inside a class and are annotated with the Test attribute. Except for the changed namespace (see below) and the changed default output, this is basically identical to the current usage of the unittest library.
use test\{Assert, Test};
class CalculatorTest {
#[Test]
public function addition() {
Assert::equals(2, (new Calculator())->add(1, 1));
}
}
To run this test, use the test subcommand:
$ xp test CalculatorTest.class.php
> [PASS] CalculatorTest
✓ addition
Tests: 1 succeeded, 0 skipped, 0 failed
Memory used: 1556.36 kB (1610.49 kB peak)
Time taken: 0.001 seconds
Naming
The library will be called test, because it can not only run unittests. The top-level package used follows this, meaning the Assert class' fully qualified name is test.Assert.
Assert DSL
The following shorthand methods exist on the Assert class:
equals(mixed $expected, mixed $actual) - check two values are equal. Uses the util.Objects::equal() method internally, which allows overwriting object comparison.
notEquals(mixed $expected, mixed $actual) - opposite of above
true(mixed $actual) - check a given value is equal to the true boolean
false(mixed $actual) - check a given value is equal to the false boolean
null(mixed $actual) - check a given value is null
instance(string|lang.Type $expected, mixed $actual) - check a given value is an instance of the given type.
Extended assertions
The Assert::that() method starts an assertion chain:
Fluent assertions
Each of the following may be chained to Assert::that():
is(unittest.assert.Condition $condition) - Asserts a given condition matches
isNot(unittest.assert.Condition $condition) - Asserts a given condition does not match
isEqualTo(var $compare) - Asserts the value is equal to a given comparison
isNotEqualTo(var $compare) - Asserts the value is not equal to a given comparison
isNull() - Asserts the value is null
isTrue() - Asserts the value is true
isFalse() - Asserts the value is false
isInstanceOf(string|lang.Type $type) - Asserts the value is of a given type
Transformation
Transforming the value before comparison can make it easier to create the value to compare against. This can be achieved by chaining map() to Assert::that():
$records= $db->open('select ...');
$expected= ['one', 'two'];
// Before
$actual= [];
foreach ($records as $record) {
$actual[]= $r['name'];
}
Assert::equals($expected, $actual);
// After
Assert::that($records)->mappedBy(fn($r) => $r['name'])->isEqualTo($expected);
Values and providers
#[Values] is a basic provider implementation. It can either return a static list of values as follows:
use test\{Assert, Test, Values};
class CalculatorTest {
#[Test, Values([[0, 0], [1, 1], [-1, 1]])]
public function addition($a, $b) {
Assert::equals($a + $b, (new Calculator())->add($a, $b));
}
}
...or invoke a method, as seen in this example:
use test\{Assert, Test, Values};
class CalculatorTest {
private function operands(): iterable {
yield [0, 0];
yield [1, 1];
yield [-1, 1];
}
#[Test, Values(from: 'operands')]
public function addition($a, $b) {
Assert::equals($a + $b, (new Calculator())->add($a, $b));
}
}
Provider implementation example
use test\Provider;
use lang\reflection\Type;
class StartServer implements Provider {
private $connection;
/** Starts a new server */
public function __construct(string $bind, ?int $port= null) {
$port??= rand(1024, 65535);
$this->connection= "Socket({$bind}:{$port})"; // TODO: Actual implementation ;)
}
/**
* Returns values
*
* @param Type $type
* @param ?object $instance
* @return iterable
*/
public function values($type, $instance= null) {
return [$this->connection];
}
}
Provider values are passed as method argument, just like #[Values], the previous only implementation.
use test\{Assert, Test};
class ServerTest {
#[Test, StartServer('0.0.0.0', 8080)]
public function connect($connection) {
Assert::equals('Socket(0.0.0.0:8080)', $connection);
}
}
Provider values are passed to the constructor and the connection can be used for all test cases. Note: The provider's values() method is invoked with $instance= null!
use test\{Assert, Test};
#[StartServer('0.0.0.0', 8080)]
class ServerTest {
public function __construct(private $connection) { }
#[Test]
public function connect() {
Assert::equals('Socket(0.0.0.0:8080)', $this->connection);
}
}
Test prerequisites
Prerequisites can exist on a test class or a test method. Unlike in the old library, they do not require the Action annotation, but stand alone.
use test\{Assert, Test};
use test\verify\{Runtime, Condition};
#[Condition('class_exists(Calculator::class)')]
class CalculatorTest {
#[Test, Runtime(php: '^8.1')]
public function addition() {
Assert::equals(2, (new Calculator())->add(1, 1));
}
}
Security considerations
n/a
Speed impact
Same
Dependencies
Related documents
Scope of Change
This RFC suggests to create a new testing library for the XP Framework. It will only work with baseless, single-instance test classes.
Rationale
extends TestCase, supporting of which costs lots of codeValuesvariations or external provider methodsAssert::that()extended assertions to simplify commonly used test codeFunctionality
In a nutshell, tests reside inside a class and are annotated with the
Testattribute. Except for the changed namespace (see below) and the changed default output, this is basically identical to the current usage of the unittest library.To run this test, use the
testsubcommand:Naming
The library will be called test, because it can not only run unittests. The top-level package used follows this, meaning the Assert class' fully qualified name is
test.Assert.Assert DSL
The following shorthand methods exist on the
Assertclass:equals(mixed $expected, mixed $actual)- check two values are equal. Uses theutil.Objects::equal()method internally, which allows overwriting object comparison.notEquals(mixed $expected, mixed $actual)- opposite of abovetrue(mixed $actual)- check a given value is equal to the true booleanfalse(mixed $actual)- check a given value is equal to the false booleannull(mixed $actual)- check a given value is nullinstance(string|lang.Type $expected, mixed $actual)- check a given value is an instance of the given type.Extended assertions
The
Assert::that()method starts an assertion chain:Fluent assertions
Each of the following may be chained to Assert::that():
is(unittest.assert.Condition $condition)- Asserts a given condition matchesisNot(unittest.assert.Condition $condition)- Asserts a given condition does not matchisEqualTo(var $compare)- Asserts the value is equal to a given comparisonisNotEqualTo(var $compare)- Asserts the value is not equal to a given comparisonisNull()- Asserts the value is nullisTrue()- Asserts the value is trueisFalse()- Asserts the value is falseisInstanceOf(string|lang.Type $type)- Asserts the value is of a given typeTransformation
Transforming the value before comparison can make it easier to create the value to compare against. This can be achieved by chaining
map()to Assert::that():Values and providers
#[Values]is a basic provider implementation. It can either return a static list of values as follows:...or invoke a method, as seen in this example:
Provider implementation example
Provider values are passed as method argument, just like
#[Values], the previous only implementation.Provider values are passed to the constructor and the connection can be used for all test cases. Note: The provider's
values()method is invoked with $instance= null!Test prerequisites
Prerequisites can exist on a test class or a test method. Unlike in the old library, they do not require the
Actionannotation, but stand alone.Security considerations
n/a
Speed impact
Same
Dependencies
Related documents