Testing
Canvas's reliance on constructor injection and plain PHP classes means every component can be tested directly with PHPUnit — no special bootstrapping, test doubles, or framework test helpers required.
Unit Testing
Because Canvas relies on constructor injection and standard PHP classes, every component can be instantiated
and tested in isolation without involving the framework at all. The test namespace is registered in
composer.json under autoload-dev:
"autoload-dev": {
"psr-4": {
"App\\Tests\\": "tests/"
}
}
Validation rules
Each validation rule is a standalone class implementing ValidationRuleInterface. Rules can be
instantiated and exercised directly — no container or request object involved:
use PHPUnit\Framework\TestCase;
use Quellabs\Canvas\Validation\Rules\Email;
use Quellabs\Canvas\Validation\Rules\Length;
use Quellabs\Canvas\Validation\Rules\NotBlank;
class EmailRuleTest extends TestCase {
public function testPassesValidAddress(): void {
$this->assertTrue((new Email())->validate('user@example.com'));
}
public function testFailsMissingAtSign(): void {
$this->assertFalse((new Email())->validate('userexample.com'));
}
public function testEmptyStringPassesByDefault(): void {
// Empty is allowed — combine with NotBlank for mandatory fields
$this->assertTrue((new Email())->validate(''));
}
}
Rules that carry configuration are tested the same way:
public function testLengthFailsStringTooShort(): void {
$this->assertFalse((new Length(min: 5))->validate('hi'));
}
public function testLengthErrorMessageContainsMinValue(): void {
$rule = new Length(min: 5, max: 20);
$rule->validate('hi');
$this->assertStringContainsString('5', $rule->getError());
}
Sanitization rules
Sanitization rules implement SanitizationRuleInterface and follow the same pattern. All rules
pass non-string values through unchanged:
use Quellabs\Canvas\Sanitization\Rules\ScriptSafe;
use Quellabs\Canvas\Sanitization\Rules\Trim;
public function testScriptSafeRemovesOnClickHandler(): void {
$rule = new ScriptSafe();
$result = $rule->sanitize('');
$this->assertStringNotContainsString('onclick', $result);
}
public function testTrimPassesThroughNonString(): void {
$this->assertSame(42, (new Trim())->sanitize(42));
}
The Validator orchestrator
Validator accepts any class implementing ValidationInterface and returns an array
of errors keyed by field name. Nested fields use dot notation:
use Quellabs\Canvas\Validation\Validator;
use Quellabs\Canvas\Validation\Contracts\ValidationInterface;
use Quellabs\Canvas\Validation\Rules\NotBlank;
use Quellabs\Canvas\Validation\Rules\Email;
class OrderFormRules implements ValidationInterface {
public function getRules(): array {
return [
'email' => [new NotBlank(), new Email()],
'address' => [
'city' => [new NotBlank()],
],
];
}
}
class ValidatorTest extends TestCase {
public function testReturnsErrorsKeyedByField(): void {
$errors = (new Validator())->validate(
['email' => 'not-an-email', 'address' => ['city' => '']],
new OrderFormRules()
);
$this->assertArrayHasKey('email', $errors);
$this->assertArrayHasKey('address.city', $errors);
}
public function testReturnsEmptyArrayWhenAllFieldsValid(): void {
$errors = (new Validator())->validate(
['email' => 'user@example.com', 'address' => ['city' => 'Haarlem']],
new OrderFormRules()
);
$this->assertEmpty($errors);
}
}
Controllers and services
Controllers are plain PHP classes. Dependencies can be mocked using PHPUnit's built-in mock builder or any compatible mock library:
class UserControllerTest extends TestCase {
public function testReturns404WhenUserNotFound(): void {
$em = $this->createMock(EntityManager::class);
$em->method('find')->willReturn(null);
$container = $this->createMock(Container::class);
$container->method('get')->willReturn($em);
$controller = new UserController($container);
$response = $controller->show(999);
$this->assertSame(404, $response->getStatusCode());
}
}
Configuration
The Configuration class is a pure value object and can be tested directly. The getAs()
method supports type casting with common string representations for booleans and comma-separated strings
for arrays:
use Quellabs\Canvas\Configuration\Configuration;
public function testGetAsBoolRecognisesStringTrue(): void {
$config = new Configuration(['debug' => 'true']);
$this->assertTrue($config->getAs('debug', 'bool'));
}
public function testGetAsArraySplitsCommaSeparatedString(): void {
$config = new Configuration(['tags' => 'php,canvas,testing']);
$this->assertSame(['php', 'canvas', 'testing'], $config->getAs('tags', 'array'));
}
Integration Testing
Unit tests cover individual components in isolation. Integration tests verify that those components work
correctly together — for example, that a controller annotated with
@InterceptWith(ValidateAspect::class) actually has its request data validated before the
method body runs, or that @WithContext causes the DI container to resolve the correct
implementation.
Canvas uses Symfony HttpFoundation for requests and responses, so
Symfony's testing utilities
work with Canvas out of the box for HTTP-level tests. Alternatively,
Codeception maps cleanly onto Canvas's three
testable layers — unit, integration, and full HTTP functional tests — with
Kernel::handle(Request) serving as the natural entry point for functional coverage.