@tomnewbyau
tomnewby.net
Comprehensive tests of the logic in a single unit
<?php
declare(strict_types=1);
// ... imports go here
final class WorkspacePermissions
{
private function __construct()
{
}
public static function canIViewThis(Workspace $workspace, User $user): bool
{
foreach ($workspace->getOrganisations() as $organisation) {
if (OrganisationPermissions::canISeeThis($organisation, $user)) {
return true;
}
}
return false;
}
}
<?php
declare(strict_types=1);
// ... imports go here
class WorkspacePermissionsTest extends TestCase
{
public function testUserCanViewWhenOrgInWorkspace(): void
{
$user = (new UserBuilder())->withOrganisation()->create();
$workspace = (new WorkspaceBuilder())->withOwner($user->getOrganisation())->create();
static::assertTrue(WorkspacePermissions::canIViewThis($workspace, $user));
}
public function testUserCannotViewWhenOrgNotInWorkspace(): void
{
$user = (new UserBuilder())->withOrganisation()->create();
$workspace = (new WorkspaceBuilder())->create();
static::assertFalse(WorkspacePermissions::canIViewThis($workspace, $user));
}
}
Good for:
Not great for:
Functional core, imperative shell
Test the core with unit tests, builders over mocks
Lots of @dataProvider based tests
Reference stats:
Combine multiple units and test them as a group
Test subject: CancelInviteService
It should: mark the invite as cancelled, notify the cancelled person and the other people working on the project via emails
<?php
class CancelInviteTest extends WebTestCase
{
function testItCancelsAndNotifies(): void
{
// ... arrange the fixture $invite
static::bootKernel();
/** @var CancelInviteService $service */
$service = static::$container->get(CancelInviteService::class);
// Act
$service->cancel($invite);
// Assert
static::assertTrue($invite->isCancelled());
/** @var StubMailer $stubMailer */
$stubMailer = static::$container->get(StubMailer::class);
static::assertTrue($stubMailer->hasOneMessageForEachRecipient(...$recipients));
}
}
Good for:
Units interacting:
General system correctness
Testing things that interact with DI + Event
Not great for:
Specific logic, edge cases:
Reference stats:
Testing web requests a real user would make, but not in a browser
<?php
// Load a page, assert on the status code
$client = static::createClient();
$client->request('GET', '/');
static::assertSame(200, $client->getResponse()->getStatusCode());
// Make a DOM crawler to inspect the page
$crawler = $client->getCrawler();
$form = $crawler->selectButton('Login')->form();
$form['_username'] = $user->getEmail();
$form['_password'] = TestParameters::DEFAULT_PASSWORD;
// Submit forms and assert you get redirected
$client->submit($form);
static::assertTrue($client->getResponse()->isRedirect()));
Good for covering:
Tend to be long, slow, cumbersome to write
We rely on these the most! Most services not that complicated
Reference stats:
slow, fragile, but...
almost exactly what your user will see
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"additionalProperties": false,
"required": ["deals", "totals"],
"properties": {
"deals": {
"type": "array",
"items": {
"$ref": "./deal.json"
}
},
"totals": {
"required": ["all", "acting"],
"type": "object",
"properties": {
"all": {
"type": "integer"
},
"acting": {
"type": "integer"
}
}
}
}
}
{
"deals": [{
"id": "some-uuid",
"name": "Krusty Burger",
...
}],
"totals": {
"all": 32,
"acting": 7
}
}
Example JSON response
Example JSON schema
Reference Stats:
Testability informs architecture
<?php
// This can be generated wherever and passed around
$messageObject = new RequestPasswordResetMessage($user, $token);
// But eventually it needs to be sent
$this->mailer->send($messageObject);
<?php
class RequestPasswordResetMessage extends AbstractPersonMessage
{
public function __construct(User $user, string $token)
{
$subject = \sprintf('%s, forgot your password?', $user->getFirstName());
parent::__construct($user, $subject);
$this->addContext([
'token' => $token,
]);
}
public function getHTMLTemplate(): string
{
return 'User/Message/request-password-reset.html.twig';
}
public function getPlainTemplate(): string
{
return 'User/Message/request-password-reset.txt.twig';
}
}
<?php
class RequestPasswordResetMessageTest extends MessageTestCase
{
public function testMessage(): void
{
// Create the message
$message = new RequestPasswordResetMessage($user, $token);
$test = (new MessageTestBuilder($variation->getMessage()))
->toEmail($user->getEmail())
->subjectContains(\sprintf('%s, forgot your password?', $user->getFirstName()))
->salutation($user)
->bodyContains(
'Forgot your password?',
'Click the link below to reset your password'
)
->link('Reset your password', 'user.password_reset.claim', ['token' => $token])
;
$this->assertMessageTest($test);
}
}
Reference Stats:
me@tomnewby.net
@tomnewbyau on Twitter