Short story about design using tests
...basically is about how i do design using tests
Let's imagine that we need register user in our app...
yeah sophisticated task
How do you design register user feature?
Yay let's plan the database schema...
and use getters and setters everywhere
No, let's plan the domain first...
bin/phpspec desc "PHPBenelux\Domain\User"
What data are needed for User to use our system?
<?php
namespace spec\PHPBenelux\Domain;
use PhpSpec\ObjectBehavior;
class UserSpec extends ObjectBehavior
{
function it_has_email_and_password()
{
$this->setEmail('leszek.prabucki@gmail.com');
$this->email()->shouldBe('leszek.prabucki@gmail.com');
$hash = password_hash('secretPass', PASSWORD_BCRYPT);
$this->setPasswordHash();
$this->passwordHash()->shouldBe($hash);
}
}
Why not use constructor instead?
<?php
namespace spec\PHPBenelux\Domain;
use PhpSpec\ObjectBehavior;
class UserSpec extends ObjectBehavior
{
function it_has_email_and_password()
{
$hash = password_hash('secretPass', PASSWORD_BCRYPT);
$this->beConstructedWith('leszek.prabucki@gmail.com', $hash);
$this->email()->shouldBe('leszek.prabucki@gmail.com');
$this->passwordHash()->shouldBe($hash);
}
}
We can add Value Objects to protect invariants
<?php
namespace spec\PHPBenelux\Domain;
use PHPBenelux\Domain\Email;
use PHPBenelux\Domain\PasswordHash;
use PHPBenelux\Domain\UserId;
use PhpSpec\ObjectBehavior;
class UserSpec extends ObjectBehavior
{
function it_has_email_and_password()
{
$hash = password_hash('secretPass', PASSWORD_BCRYPT);
$this->beConstructedWith(
UserId::generate(),
new Email('leszek.prabucki@gmail.com'),
PasswordHash::fromHash($hash)
);
$this->id()->shouldHaveType(UserId::class);
$this->email()->shouldBeLike(new Email('leszek.prabucki@gmail.com'));
$this->passwordHash()->shouldBeLike(PasswordHash::fromHash($hash));
}
}
bin/phpspec run
<?php
declare(strict_types=1);
namespace PHPBenelux\Domain;
class User
{
public function __construct($argument1, $argument2, $argument3)
{
// TODO: write logic here
}
public function id()
{
// TODO: write logic here
}
}
<?php
declare(strict_types=1);
namespace PHPBenelux\Domain;
class User
{
private $id;
private $email;
private $passwordHash;
public function __construct(UserId $id, Email $email, PasswordHash $passwordHash)
{
$this->id = $id;
$this->email = $email;
$this->passwordHash = $passwordHash;
}
public function id(): UserId
{
return $this->id;
}
public function email(): Email
{
return $this->email;
}
public function passwordHash(): PasswordHash
{
return $this->passwordHash;
}
}
Ok, what now?
Register User service
<?php
namespace spec\PHPBenelux\Application\UseCase;
use Exception;
use InvalidArgumentException;
use PHPBenelux\Application\UseCase\RegisterUser;
use PHPBenelux\Application\TransactionManager;
use PHPBenelux\Domain\Users;
use PHPBenelux\Domain\UserFactory;
use PHPBenelux\Domain\User;
use PhpSpec\ObjectBehavior;
class RegisterUserSpec extends ObjectBehavior
{
function let(
TransactionManager $transactionManager,
Users $users,
UserFactory $userFactory
) {
$this->beConstructedWith($transactionManager, $users, $userFactory);
}
function it_creates_and_store_user_in_repository(
TransactionManager $transactionManager,
Users $users,
UserFactory $userFactory,
User $user
) {
$transactionManager->begin()->shouldBeCalled();
$userFactory->create(
'leszek.prabucki@gmail.com',
'password'
)->willReturn($user);
$users->has($user)->willReturn(false);
$users->add($user)->shouldBeCalled();
$transactionManager->commit()->shouldBeCalled();
$this->execute(new RegisterUser\Command(
'leszek.prabucki@gmail.com',
'password'
));
}
}
<?php
declare(strict_types=1);
namespace PHPBenelux\Application\UseCase;
use PHPBenelux\Application\TransactionManager;
use PHPBenelux\Domain\UserFactory;
use PHPBenelux\Domain\Users;
use Throwable;
class RegisterUser
{
private $transactionManager;
private $users;
private $userFactory;
public function __construct(TransactionManager $transactionManager, Users $users, UserFactory $userFactory)
{
$this->transactionManager = $transactionManager;
$this->users = $users;
$this->userFactory = $userFactory;
}
public function execute(RegisterUser\Command $command): void
{
$this->transactionManager->begin();
try {
$user = $this->userFactory->create($command->email(), $command->password());
if ($this->users->has($user)) {
$this->transactionManager->rollback();
return;
}
$this->users->add($user);
$this->transactionManager->commit();
} catch (Throwable $exception) {
$this->transactionManager->rollback();
throw $exception;
}
}
}
Doctrine Adapter
<?php
declare(strict_types=1);
namespace PHPBenelux\Adapter\Doctrine\Domain;
use Doctrine\ORM\EntityManagerInterface;
use PHPBenelux\Domain\Email;
use PHPBenelux\Domain\Exception\UserNotFound;
use PHPBenelux\Domain\Users as UsersInterface;
final class Users implements UsersInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function add(User $user): void
{
$this->entityManager->persist($user);
}
public function has(User $user): bool
{
return (bool) $this
->entityManager
->getRepository(User::class)
->findOneBy(['email.email' => (string) $user->email()])
;
}
public function get(Email $email): User
{
/** @var User $user */
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email.email' => (string) $email]);
if (!$user) {
throw new UserNotFound();
}
return $user;
}
}
<?php
declare(strict_types=1);
namespace PHPBenelux\Adapter\Doctrine\Application;
use Doctrine\ORM\EntityManagerInterface;
use PHPBenelux\Application\TransactionManager as TransactionManagerInterface;
final class TransactionManager implements TransactionManagerInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function begin(): void
{
$this->entityManager->beginTransaction();
}
public function commit(): void
{
$this->entityManager->flush();
$this->entityManager->commit();
}
public function rollback(): void
{
$this->entityManager->rollback();
}
}
Thank you!
Short story about design using tests PHPBenelux uncon
By Leszek Prabucki
Short story about design using tests PHPBenelux uncon
- 1,826