Testing
- Introduction
- Environment
- Creating Tests
- Application Testing
- Working With Databases
- Mocking
- Running Tests
Introduction
Framework is built with testing in mind. In fact, support for testing with PHPUnit is included out of the box and a phpunit.xml file is already set up for your application. The framework also ships with convenient helper methods that allow you to expressively test your applications.
The make:test command places tests in two directories of your application’s tests directory: Feature and Unit. Unit tests are tests that focus on a very small, isolated portion of your code. In fact, most unit tests probably focus on a single method. Tests within your “Unit” test directory do not boot your Framework application and therefore are unable to access your application’s database or other framework services.
Feature tests may test a larger portion of your code, including how several objects interact with each other or even a full HTTP request to a JSON endpoint. Generally, most of your tests should be feature tests. These types of tests provide the most confidence that your system as a whole is functioning as intended.
An ExampleTest.php file is provided in the tests directory. After installing a new Framework application, execute the vendor/bin/phpunit command to run your tests.
Environment
When running tests, Framework will automatically set the configuration environment to testing because of the environment variables defined in the phpunit.xml file. Framework also automatically configures the cache to the array driver so that no cache data will be persisted while testing.
You are free to define other testing environment configuration values as necessary. The testing environment variables may be configured in your application’s phpunit.xml file, but make sure to clear your configuration cache using the config:clear Run command before running your tests!
The CreatesApplication Trait
Framework does not include a CreatesApplication trait: your application’s base TestCase class (tests/TestCase.php) contains a createApplication method that bootstraps the Framework application before running your tests. Framework’s parallel testing feature uses a Tests\CreatesApplication trait if you define one; otherwise it requires your application’s bootstrap/app.php file.
Creating Tests
To create a new test case, use the make:test Run command. By default, tests will be placed in the tests/Feature directory:
php run make:test UserTest
If you would like to create a test within the tests/Unit directory, you may use the --unit option when executing the make:test command:
php run make:test UserTest --unit
If you would like to create a Pest PHP test, you may provide the --pest option to the make:test command:
php run make:test UserTest --pest
php run make:test UserTest --unit --pest
Once the test has been generated, you may define test methods as you normally would using PHPUnit. To run your tests, execute the vendor/bin/phpunit command from your terminal:
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ExampleTest extends TestCase
{
/**
* A basic test example.
*/
public function test_basic_test(): void
{
$this->assertTrue(true);
}
}
[!WARNING]
If you define your ownsetUp/tearDownmethods within a test class, be sure to call the respectiveparent::setUp()/parent::tearDown()methods on the parent class. Typically, you should invokeparent::setUp()at the start of your ownsetUpmethod, andparent::tearDown()at the end of yourtearDownmethod.
Application Testing
Framework provides a very fluent API for making HTTP requests to your application and examining the output.
Authentication
The actingAs helper method provides a simple way to authenticate a given user as the current user:
<?php
class ExampleTest extends TestCase
{
public function testApplication()
{
$user = \Database\Factories\UserFactory::new()->create();
$this->actingAs($user)
->get('/user');
}
}
Custom HTTP Requests
If you would like to make a custom HTTP request into your application and get the full MacropaySolutions\KernelDev\Testing\TestResponse object, you may use the call method:
public function testApplication()
{
$response = $this->call('GET', '/');
$this->assertEquals(200, $response->status());
}
If you are making POST, PUT, or PATCH requests you may pass an array of input data with the request. Of course, this data will be available in your routes and controller via the Request instance:
$response = $this->call('POST', '/user', ['name' => 'Name']);
JSON Validation Errors
If you are using the assertJsonValidationErrors method, you should pass null as the response key that contains the response’s error message. Unlike Kernel, Framework returns errors at the root of the returned JSON object:
public function testApplication()
{
$response = $this->call('POST', '/user', ['name' => null]);
$response->assertJsonValidationErrors('name', $responseKey = null);
}
Working With Databases
Framework also provides a variety of helpful tools to make it easier to test your database driven applications. First, you may use the seeInDatabase helper to assert that data exists in the database matching a given set of criteria. For example, if we would like to verify that there is a record in the users table with the email value of sally@example.com, we can do the following:
public function testDatabase()
{
// Make call to application...
$this->seeInDatabase('users', ['email' => 'sally@foo.com']);
}
Of course, the seeInDatabase method and other helpers like it are for convenience. You are free to use any of PHPUnit’s built-in assertion methods to supplement your tests.
Resetting The Database After Each Test
It is often useful to reset your database after each test so that data from a previous test does not interfere with subsequent tests.
Using Migrations
One option is to rollback the database after each test and migrate it before the next test. Framework provides a simple DatabaseMigrations trait that will automatically handle this for you. Simply use the trait on your test class:
<?php
use MacropaySolutions\KernelDev\Framework\Testing\DatabaseMigrations;
use MacropaySolutions\KernelDev\Framework\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseMigrations;
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
$this->get('/foo');
}
}
Using Transactions
Another option is to wrap every test case in a database transaction. Again, Framework provides a convenient DatabaseTransactions trait that will automatically handle this:
<?php
use MacropaySolutions\KernelDev\Framework\Testing\DatabaseMigrations;
use MacropaySolutions\KernelDev\Framework\Testing\DatabaseTransactions;
class ExampleTest extends TestCase
{
use DatabaseTransactions;
/**
* A basic functional test example.
*
* @return void
*/
public function testBasicExample()
{
$this->get('/foo');
}
}
Model Factories
When testing, it is common to need to insert a few records into your database before executing your test. Instead of manually specifying the value of each column when you create this test data, Framework allows you to define a default set of attributes for each of your Obvious models using model factories.
To get started, take a look at the database/factories/UserFactory.php file in your application. Out of the box, this file contains the following factory definition:
<?php
namespace Database\Factories;
use App\Models\User;
use MacropaySolutions\KernelDev\Database\Obvious\Factories\Factory;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = User::class;
/**
* Define the model's default state.
*
* @return array
*/
public function definition()
{
return [
'name' => $this->faker->name,
'email' => $this->faker->unique()->safeEmail,
];
}
}
As you can see, in their most basic form, factories are classes that extend Framework’s base factory class and define a model property and definition method. The definition method returns the default set of attribute values that should be applied when creating a model using the factory.
Via the faker property, factories have access to the Faker PHP library, which allows you to conveniently generate various kinds of random data for testing.
Factory States
State manipulation methods allow you to define discrete modifications that can be applied to your model factories in any combination. For example, your User model might have a suspended state that modifies one of its default attribute values. You may define your state transformations using the base factory’s state method. You may name your state method anything you like. After all, it’s just a typical PHP method:
/**
* Indicate that the user is suspended.
*
* @return \MacropaySolutions\KernelDev\Database\Obvious\Factories\Factory
*/
public function suspended()
{
return $this->state([
'account_status' => 'suspended',
]);
}
If your state transformation requires access to the other attributes defined by the factory, you may pass a callback to the state method. The callback will receive the array of raw attributes defined for the factory:
/**
* Indicate that the user is suspended.
*
* @return \MacropaySolutions\KernelDev\Database\Obvious\Factories\Factory
*/
public function suspended()
{
return $this->state(function (array $attributes) {
return [
'account_status' => 'suspended',
];
});
}
Factory Callbacks
Factory callbacks are registered using the afterMaking and afterCreating methods and allow you to perform additional tasks after making or creating a model. You should register these callbacks by defining a configure method on the factory class. This method will automatically be called by Kernel when the factory is instantiated:
namespace Database\Factories;
use App\Models\User;
use MacropaySolutions\KernelDev\Database\Obvious\Factories\Factory;
use MacropaySolutions\Kernel\Support\Str;
class UserFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = User::class;
/**
* Configure the model factory.
*
* @return $this
*/
public function configure()
{
return $this->afterMaking(function (User $user) {
//
})->afterCreating(function (User$user) {
//
});
}
// ...
}
Mocking
Mocking Events
If you are making heavy use of Framework’s event system, you may wish to silence or mock certain events while testing. For example, if you are testing user registration, you probably do not want all of a UserRegistered event’s handlers firing, since these may send “welcome” e-mails, etc.
Framework provides a convenient expectsEvents method that verifies the expected events are fired, but prevents any handlers for those events from running:
<?php
class ExampleTest extends TestCase
{
public function testUserRegistration()
{
$this->expectsEvents('App\Events\UserRegistered');
// Test user registration code...
}
}
If you would like to prevent all event handlers from running, you may use the withoutEvents method:
<?php
class ExampleTest extends TestCase
{
public function testUserRegistration()
{
$this->withoutEvents();
// Test user registration code...
}
}
Mocking Jobs
Sometimes, you may wish to simply test that specific jobs are dispatched by your controllers when making requests to your application. This allows you to test your routes / controllers in isolation - set apart from your job’s logic. Of course, you can then test the job itself in a separate test class.
Framework provides a convenient expectsJobs method that will verify that the expected jobs are dispatched, but the job itself will not be executed:
<?php
class ExampleTest extends TestCase
{
public function testPurchasePodcast()
{
$this->expectsJobs('App\Jobs\PurchasePodcast');
// Test purchase podcast code...
}
}
[!WARNING]
Because PHP-Framework enforces strict architectural boundaries, closure serialization is completely unsupported and forbidden. When validating or defining queued jobs and their execution payloads, you must strictly utilize Storable Array Callables (e.g.,[Class::class, 'method']) instead of closures.
[!NOTE]
Storable Array Callables & Mocking: If your application dispatches background tasks using Storable Array Callables (e.g.,\dispatch([PodcastService::class, 'purchase'])), pass the service class name (e.g.,PodcastService::class) to theexpectsJobsmethod. The testing engine will automatically unwrap the payload to verify the dispatch.Note on Multiple Jobs: The
expectsJobsmethod expects a single job class string per call. To assert multiple jobs, chain them or call the helper sequentially (e.g.,$this->expectsJobs(JobA::class)->expectsJobs(JobB::class)).
Note: This method only detects jobs that are dispatched via the
dispatchglobal helper function or the$this->dispatchmethod from a route or controller. It does not detect jobs that are sent directly toQueue::push. Always useapp('queue')ordispatch()to queue your jobs.
Running Tests
As mentioned previously, once you’ve written tests, you may run them using phpunit:
./vendor/bin/phpunit
Running Tests in Parallel
By default, Framework and PHPUnit execute your tests sequentially within a single process. However, you may greatly reduce the amount of time it takes to run your tests by running tests simultaneously across multiple processes. To get started, you should install the brianium/paratest Composer package as a “dev” dependency. Then, run paratest with the MacropaySolutions\KernelDev\Testing\ParallelRunner runner and the FRAMEWORK_PARALLEL_TESTING environment variable set:
composer require brianium/paratest --dev
FRAMEWORK_PARALLEL_TESTING=1 ./vendor/bin/paratest --runner '\MacropaySolutions\KernelDev\Testing\ParallelRunner'
By default, paratest will create as many processes as there are available CPU cores on your machine. However, you may adjust the number of processes using the --processes option:
FRAMEWORK_PARALLEL_TESTING=1 ./vendor/bin/paratest --runner '\MacropaySolutions\KernelDev\Testing\ParallelRunner' --processes=4
[!WARNING]
When running tests in parallel, some PHPUnit options (such as--do-not-cache-result) may not be available.
Parallel Testing and Databases
As long as you have configured a primary database connection, Framework automatically handles creating and migrating a test database for each parallel process that is running your tests. The test databases will be suffixed with a process token which is unique per process. For example, if you have two parallel test processes, Framework will create and use your_db_test_1 and your_db_test_2 test databases.
By default, test databases persist between parallel runs so that they can be used again by subsequent runs. However, you may re-create them by setting the FRAMEWORK_PARALLEL_TESTING_RECREATE_DATABASES environment variable:
FRAMEWORK_PARALLEL_TESTING=1 FRAMEWORK_PARALLEL_TESTING_RECREATE_DATABASES=1 ./vendor/bin/paratest --runner '\MacropaySolutions\KernelDev\Testing\ParallelRunner'
Parallel Testing Hooks
Occasionally, you may need to prepare certain resources used by your application’s tests so they may be safely used by multiple test processes.
To safely initialize parallel testing without polluting your production code or causing errors when running optimization tasks via --no-dev, testing-specific service providers must never be registered inside application service providers. Instead, register the provider and declare its hooks directly inside your application’s base TestCase or environment bootstrapping configuration layer (e.g., tests/TestCase.php):
<?php
namespace Tests;
use MacropaySolutions\KernelDev\Framework\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
/**
* Bootstraps the application and isolates parallel testing logic.
*
* @return \MacropaySolutions\Framework\Application
*/
public function createApplication()
{
$app = require __DIR__ . '/../bootstrap/app.php';
// Safely register the testing provider exclusively during test suite runs
$app->register(\MacropaySolutions\KernelDev\Testing\ParallelTestingServiceProvider::class);
// Resolve the manager to safely attach parallel task workflows
$parallelTesting = $app->make(\MacropaySolutions\KernelDev\Testing\ParallelTesting::class);
$parallelTesting->setUpProcess(function (int $token) {
// ...
});
$parallelTesting->setUpTestCase(function (int $token, BaseTestCase $testCase) {
// ...
});
// Executed when a test database is created...
$parallelTesting->setUpTestDatabase(function (string $database, int $token) {
app(\MacropaySolutions\Kernel\Contracts\Console\Kernel::class)->call('db:seed');
});
$parallelTesting->tearDownTestCase(function (int $token, BaseTestCase $testCase) {
// ...
});
$parallelTesting->tearDownProcess(function (int $token) {
// ...
});
return $app;
}
}
Accessing the Parallel Testing Token
If you would like to access the current parallel process “token” from any other location in your application’s test code, you may resolve it via the global helper. This token is a unique, string identifier for an individual test process and may be used to segment resources across parallel test processes. For example, Framework automatically appends this token to the end of the test databases created by each parallel testing process:
$token = \app(\MacropaySolutions\KernelDev\Testing\ParallelTesting::class)->token();