Revision: Sat, 27 Apr 2024 17:15:54 GMT
v3.8 – outdated
This version of the documentation is outdated. Consider upgrading your project to Spiral Framework 3.12
Edit this page

Testing — Database

Spiral offers a powerful tool for managing data in your application through the use of the spiral-packages/database-seeder package. This package allows you to easily seed test data into their CycleORM entities using seed classes. It provides the ability to create entities through the use of factories.

Danger
It's important to always use a separate test database for running your tests, and never use a production database for testing. Using a production database for testing can result in data loss or corruption, and can also cause unexpected behavior in the production system. Always create a separate test database and use it exclusively for testing.

Installation

To install the database seeder package, run the following command:

composer require spiral-packages/database-seeder --dev

Note
It's important to note that the --dev flag is used because the package is intended for use in development and testing environments.

After package install you need to register bootloader from the package.

php
app/src/Application/Kernel.php
protected const LOAD = [
    // ...
    \Spiral\DatabaseSeeder\Bootloader\DatabaseSeederBootloader::class,
];

We also recommend you to add the following to your composer.json file. This allows the package to automatically discover and load the classes you create, so you don't have to manually include them in your code.

json
{
  // ...
  "autoload-dev": {
    "psr-4": {
      "Database\\": "app/database/"
    }
  }
}

After these steps, you have installed the package and registered it's bootloader, so you can now use Spiral's database seeder package in your application.

Defining Seed Factory

To define a seed factory, you should create a class that extends the Spiral\DatabaseSeeder\Factory\AbstractFactory class.

Here is an example of a seed factory that defines a User entity:

php
namespace Database\Factory;

use App\Entity\User;
use Spiral\DatabaseSeeder\Factory\AbstractFactory;
use Spiral\DatabaseSeeder\Factory\FactoryInterface;

/**
 * @implements FactoryInterface<User>
 */
final class UserFactory extends AbstractFactory
{
    public function entity(): string
    {
        return User::class;
    }
    
    public function makeEntity(array $definition): User
    {
        return new User(
            username: $definition['username']
        );
    }

    public function definition(): array
    {
        return [
            'firstName' => $this->faker->firstName(),
            'lastName' => $this->faker->lastName(),
            'birthday' => \DateTimeImmutable::createFromMutable($this->faker->dateTime()),
            'comments' => CommentFactory::new()->times(3)->make(), // Can use other factories.
            // Be careful, circular dependencies are not allowed!
        ];
    }
}

Note
Since v2.4.0 you can define the return type for a factory @implements FactoryInterface<...> annotation as in the example above. With this annotation in place, your IDE will now provide suggestive autocomplete options, making the code interaction more intuitive and error-prone. image

The entity method should return the fully qualified class name of the target entity that the factory is responsible for creating. In some cases, the factory may use the class name returned by the entity method to create a new instance of the entity without calling its constructor. Instead, it may use reflection to directly set the properties of the entity using data from the definition method.

The makeEntity method allows you to control the process of creating an entity through its constructor. The method takes an array of definitions as an argument, which is generated by the definition method.

The definition method is where you can define all of the properties of the entity. The method should return an array where the keys are the property names and the values are the values that should be set for those properties. These values can be hard-coded or generated using the Faker library, which provides a wide range of fake data generation methods such as names, addresses, phone numbers, etc. This method is responsible for generating the definition array that will be passed to the makeEntity method to construct the entity object or used to set properties directly.

Using Seed Factory

A factory can be created by calling the new method on the factory class:

php
use Database\Factory\UserFactory;

$factory = UserFactory::new();

This will create a new instance of the factory. It provides several useful methods for generating entities.

You can also pass an array of definition to the new method of the factory class.

php
use Database\Factory\UserFactory;

$factory = UserFactory::new(['admin' => true]);

This is a useful feature when you have a common set of definitions that you want to use across multiple factories or when you want to set a default value for a property that will be overridden in specific cases.

Create entities

The create method creates an array of entities, stores them in the database and returns them for further use in the code.

php
$users = $factory->times(10)->create();

The createOne method creates a single entity, stores it in the database and returns it for further use in the code.

php
$user = $factory->createOne();

Make entities

The make method creates an array of entities and returns them for further use in code, but does not store them in the database.

php
$users = $factory->times(10)->make();

The makeOne method creates a single entity and returns it for further use in code, but does not store it in the database.

php
$user = $factory->makeOne();

Entity states

The state method allows developers to easily define specific states for entities. It can be used to set a specific set of properties on an entity when it is created. The returned values from the closure will replace the corresponding values in the definition array, allowing developers to easily change the state of an entity in a specific way.

Note
It is non-destructive, it will only update the properties passed in the returned array and will not remove any properties from the definition array.

php
$factory->state(fn(\Faker\Generator $faker, array $definition) => [
    'admin' => $faker->boolean(),
])->times(10)->create();

In addition to the state method, there also the entityState method. This method allows developers to change the state of an entity object using the available methods of that entity. It takes a closure as an argument, which should accept the entity as an argument and should return the modified entity. This allows developers to take full advantage of the object-oriented nature of their entities and use the methods that are already defined on the entity to change its state.

php
$factory->entityState(static function(User $user) {
    return $user->markAsDeleted();
})->times(10)->create();

The state and entityState methods can be used inside a factory class to create additional methods for creating entities with specific states. By creating these additional methods, developers can create a more expressive and readable code, and make it easier to understand the intent of the code.

php
final class UserFactory extends AbstractFactory
{
    // ....

    public function admin(): self
    {
        return $this->state(fn(\Faker\Generator $faker, array $definition) => [
            'admin' => true,
        ]);
    }
    
    public function fromCity(string $city): self
    {
        return $this->state(fn(\Faker\Generator $faker, array $definition) => [
            'city' => $city,
        ]);
    }
    
    public function deleted(): self
    {
        return $this->entityState(static function (User $user) {
            return $user->markAsDeleted();
        });
    }
    
    public function withBirthday(\DateTimeImmutable $date): self
    {
        return $this->entityState(static function (User $user) use ($date) {
            $user->birthday = $date;
            return $user;
        });
    }
}

And example of using these methods:

php
$factory->admin()
    ->fromCity('New York')
    ->deleted()
    ->withBirthday(new \DateTimeImmutable('2010-01-01 00:00:00'))
    ->times(10)
    ->create();

Factories can be used in your feature test cases to create entities in the database without seeding. This can be useful in situations where you want to create a specific set of test data for a feature test, or when you want to test the behavior of your application with a specific set of data.

Seeding

The package provides the ability to seed the database with test data. To do this, developers can create a Seeder class and extend it from the Spiral\DatabaseSeeder\Seeder\AbstractSeeder class. The Seeder class should implement the run method which returns a generator with entities to store in the database.

php
namespace Database\Seeder;

use Spiral\DatabaseSeeder\Seeder\AbstractSeeder;

final class UserTableSeeder extends AbstractSeeder
{
    public function run(): \Generator
    {
        foreach (UserFactory::new()->times(100)->make() as $user) {
            yield $user;
        }
        
        foreach (UserFactory::new()->admin()->times(10)->make() as $user) {
            yield $user;
        }
        
        foreach (UserFactory::new()->admin()->deleted()->times(1)->make() as $user) {
            yield $user;
        }
    }
}

Seeders are primarily used to fill the database with test data for your stage server, providing a consistent set of data for the developers and stakeholders to test and use.

Seeders are especially useful when testing complex applications that have many different entities and relationships between them. By using seeders to populate the database with test data, you can ensure that your tests are run against a consistent and well-defined set of data, which can help to make your tests more reliable and less prone to flakiness.

Running seeders

Use the db:seed command to run the seeders.

php app.php db:seed

This will execute all of the seeder classes that are registered with the command and insert the test data into the database.

Testing

The package provides several additional features for easier testing of applications with databases. To use these features, your application's tests must be written using the spiral/testing package.

An example of how to use the base test class in your test cases is shown below:

php
namespace Tests\Feature;

abstract class DatabaseTestCase extends \Spiral\DatabaseSeeder\TestCase
{
}

Refresh database

The RefreshDatabase trait is one of the provided traits that allows you to easily set up and tear down a test database for your application's tests.

When you use this trait in your test cases, it creates the database structure on the first run and wraps the test execution into a transaction. After the test runs, the transaction is rollbacked, but the database structure is saved for use in the next test.

For example, you can use this trait in your test cases as shown below:

php
namespace Tests\Feature;

use Spiral\DatabaseSeeder\Database\Traits\RefreshDatabase;

abstract class DatabaseTestCase extends \Spiral\DatabaseSeeder\TestCase
{
    use RefreshDatabase;
}

Database migrations

The DatabaseMigrations trait is another provided trait that allows you to easily set up and tear down a test database for your application's tests.

When you use this trait in your test cases, it creates the database structure, performs a test, and completely rollbacks the state of the database. This means that any changes made to the database during the test will be discarded.

For example, you can use this trait in your test cases as shown below:

php
namespace Tests\Feature;

use Spiral\DatabaseSeeder\Database\Traits\DatabaseMigrations;

abstract class DatabaseTestCase extends \Spiral\DatabaseSeeder\TestCase
{
    use DatabaseMigrations;
}

Assertions

The \Spiral\DatabaseSeeder\TestCase class provides several additional assertion methods that allow you to check data in the database during tests.

Here are the methods and their functionality:

  • assertTableExists($table): This method checks if the table with the given name exists in the database.
  • assertTableIsNotExists($table): This method checks if the table with the given name does not exist in the database.
  • assertTableCount($table, $count): This method checks if the table with the given name has a certain number of records.
  • assertTableHas($table, $condition): This method checks if there is a record in the table with the given name that matches a certain condition.
  • assertEntitiesCount($entity, $count): This method checks if the given entity has a certain number of records. It's the same as assertTableCount but checks by entity, not by table name.
  • assertTableHasEntity($entity, $condition): This method checks if there is a record in the table associated with the given entity that matches a certain condition. It's the same as assertTableHas but checks by entity, not by table name.

Console commands

The package provides several console commands to quickly create a factory, seeder. These commands make it easy to quickly create the necessary classes for seeding and testing your application, without having to manually create the files or ensure that the necessary boilerplate code is included.

Create a factory

The Spiral\DatabaseSeeder\Console\Command\FactoryCommand console command is used to create a factory. The name of the factory is passed as an argument.

For example, to create a factory named UserFactory, the command would be:

php app.php create:factory UserFactory

Create a seeder

The Spiral\DatabaseSeeder\Console\Command\SeederCommand console command is used to create a seeder. The name of the seeder is passed as an argument.

For example, to create a seeder named UserSeeder, the command would be:

php app.php create:seeder UserSeeder