Juan Pablo Ramirez - Nicolas Masson
Test Fixtures
DB clean-up
Arrange
Act
Assert
Users ↔ UsersGroups ↔ Permissions
users
users_users_groups
users_groups
users_groups_permissions
permissions
Arrange
Create user, user group, permission Foo, pivot tables
Act
Check permission Foo for user
Assert
Expect user to have permission Foo
namespace App\Model\Table;
use Cake\ORM\Table;
class UsersTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('UsersGroups');
}
}
namespace App\Model\Table;
use Cake\ORM\Table;
class UsersGroupsTable extends Table
{
public function initialize(array $config): void
{
$this
->belongsToMany('Users')
->belongsToMany('Permissions');
}
}
namespace App\Model\Table;
use Cake\ORM\Table;
class PermissionsTable extends Table
{
public function initialize(array $config): void
{
$this->belongsToMany('UsersGroups');
}
}
// Retrieve all users, join permissions
$users = $this->Users->find()
->contain('UsersGroups.Permissions')
->toArray();
// Filter users by permissions, join permissions
$admins = $this->Users->find()
->matching('UsersGroups.Permissions', function ($q) {
return $q->where(['Permissions.name' => 'Admin')
})
->toArray();
// With Database access
public function hasPermission(int $userId, string $perm): bool
{
return $this->Users->find()
->innerJoinWith('UsersGroups.Permissions', function ($q) use ($perm) {
return $q->where(['Permissions.name' => $perm)
})
->where(['Users.id' => $userId])
->count() > 0;
}
// Without Database access
public function hasPermission(User $user, string $perm): bool
{
return in_array(
$perm,
Hash::extract($user, 'users_groups.{n}.permissions.{n}.name')
)
}
UsersTable ↔ UsersGroupsTable ↔ PermissionsTable
5 Tables → 5 Fixtures per test
users
users_users_groups
users_groups
users_groups_permissions
permissions
1 permission Foo - 4 users: Admins, Gurus, Foo, Bar
App\Test\Factory\UserFactory.php
protected function getRootTableRegistryName(): string
{
return 'Users';
}
protected function setDefaultTemplate(): void
{
$this->setDefaultData(function(Generator $faker) {
return [
'username' => $faker->username,
'email' => $faker->email,
];
});
$this->withPermission();
}
$user = UserFactory::make()->getEntity();
UserFactory::make(4)->getEntities();
UserFactory::make(4)->persist();
UserFactory::make(['username' => 'Foo'])->persist();
UserFactory::make([
['username' => 'Foo'],
['username' => 'Bar'],
])->persist();
UserFactory::make(3)
->with('UsersGroups.Permissions', ['name' => 'Foo'])
->getEntity();
UserFactory::make()
->with('UsersGroups.Permissions', [
['name' => 'Foo'],
['name' => 'Bar'],
])
->getEntity();
UserFactory::make()
->withPermission('Foo')
->getEntity();
public function testUserPermission()
{
// Arrange
$user = UserFactory::make()
->with('UsersGroups.Permissions', ['name' => 'Foo'])
->getEntity();
// Act
$act = $this->Users->hasPermission($user, 'Foo');
// Assert
$this->assertTrue($act);
}
public function dataProviderForTestPermission()
{
return [['Admin',true],['Guru',true],['Foo',true],['Bar',false]];
}
/** @dataProvider dataProviderForTestPermission */
public function testPermission(string $p, bool $exp)
{
$user = UserFactory::make()->withPermission($p)->getEntity();
$act = $this->Users->hasPermission($user, 'Foo');
$this->assertSame($exp, $act);
}
Feature: User permission
Background:
Given I create a user with id 504
Scenario:
Given I log in with permission 'Users'
When I call get 'users/view/504'
Then I shall be granted access.
Scenario:
Given I log in with permission 'Admin'
When I call get 'users/view/504'
Then I shall be granted access.
Scenario:
Given I log in with permission 'Guru'
When I call get 'users/view/504'
Then I shall be granted access.
Scenario:
Given I log in with permission 'Foo'
When I call get 'users/view/504'
Then I shall be redirected.
Default template
Association builder
Persist or not
Random Ids
Time (Y axis) vs Number of tests (X axis)
https://github.com/vierge-noire/test-database-cleaner
Test suite starts
Truncate all tables
Load test dump
High maintenance
Run migrations
~10 seconds
Start test
Start SQL transaction
Run test
Rollback transaction
Test suite starts
Run migrations
CREATE TABLE IF NOT EXISTS dirty_tables_collector (table_name VARCHAR(128) PRIMARY KEY);
CREATE TRIGGER table_users_trigger AFTER INSERT ON `users`
FOR EACH ROW
INSERT IGNORE INTO dirty_table_collector (table_name)
VALUES ('dirty_tables_collector'), ('users');
Truncate dirty tables in 1 procedure
Run test
https://github.com/vierge-noire/test-database-cleaner
It's fast: no warm up
MySQL, Postgres, SQL Server, Sqlite + contribution
CakePHP, Laravel + contribution
Feel free to join the project!
Build fast with CakePHP
Grow solid with the Fixture Factories
Join us to improve the test database cleaner
Credits: Juan Pablo Ramirez - Nicolas Masson