reedware/container-testcase

Enables unit testing with an empty service container.

v1.1.0 2023-07-24 13:20 UTC

This package is auto-updated.

Last update: 2024-04-24 14:52:53 UTC


README

Laravel Version Automated Tests Coding Standards Code Coverage Static Analysis Latest Stable Version

This package enables unit testing with an empty service container.

Table of Contents

Introduction

I'm a huge advocate for unit testing, but when developing packages for Laravel, I often find myself needing the service container within my unit tests. This package simply implements a pattern I've been following for years, but never defined in a shared location.

This approach to testing keeps your test cases light, as you don't have to boot the entire Laravel application, but you still get some quality of life features necessary for testing services created as a part of package development.

Installation

Install this package using Composer:

composer require reedware/container-testcase --dev

Take note of the --dev flag. This package is intended for testing. As such, it should only be required in dev settings.

Next, you have two options: Extension or Trait.

1. Extension

Change your test case to extend from Reedware\ContainerTestCase\ContainerTestCase.

use Reedware\ContainerTestCase\ContainerTestCase;

class TestCase extends ContainerTestCase
{
    /* ... */
}

2. Trait

Add the ServiceContainer trait to your test case.

use PHPUnit\Framework\TestCase as BaseTestCase;
use Reedware\ContainerTestCase\ServiceContainer;

class TestCase extends BaseTestCase
{
    use ServiceContainer;
}

Be mindful that the trait overrides the setUp() and tearDown() methods. If you have your own definition, you'll need to call the setUp/tearDown functionality provided by the trait.

protected function setUp(): void
{
    parent::setUp();

    $this->setUpServiceContainer();
}

protected function tearDown(): void
{
    $this->tearDownServiceContainer();

    parent::tearDown();
}

Usage

1. Service Provider

If you're writing a package that binds into the service container, chances are, you have a service provider. You'll want to register your service provider during the setup process:

protected function setUp(): void
{
    parent::setUp();

    $this->registerServiceProvider(MyServiceProvider::class);
}

You can do something similar for booting:

protected function setUp(): void
{
    parent::setUp();

    $this->bootServiceProvider(MyServiceProvider::class);
}

Any dependencies in your boot() method will be injected from the service container, exactly how the Laravel Framework does it.

2. The Application Instance

The application instance in your test is not the same as the one provided by the Laravel Framework. The provided application instance offers full container functionality, but throws by default on any non-container method (e.g. $this->app->version()). If you need to use these methods in your service providers, you can provide expectations to the application instance, as it also acts as a partial mock instance.

/** @test */
public function it_bails_on_production(): void
{
    $this->app
        ->shouldReceive('environment')
        ->withNoArgs()
        ->once()
        ->andReturn('production');

    $this->registerServiceProvider(MyServiceProvider::class);

    $this->assertFalse($this->app->bound(MyService::class));
}

3. The Service Container

The application instance acts as your service container. Similar to Laravel's feature test, some mocking and container helpers are available as methods on your test cases:

Make

Creates a new instance of the specified service using the container. Alias for $this->app->make(...).

Usage:

$service = $this->make(MyService::class);

Mock

Creates a mock of the specified service and binds it to the container.

Usage:

$mock = $this->mock(MyService::class, function (MockInterface $mock) {
    $mock
        ->shouldReceive(...)
        ->...
});

or

$mock = $this->mock(MyService::class);

$mock
    ->shouldReceive(...)
    ->...

Note: This method binds the mock instance to MyService::class. If you just want a mocked service without binding it to the container, use Mockery::mock(MyService::class).

Mock As

Creates a mock of the specified service and binds it to the container under the given alias. The $this->mock(...) method will bind the service to the container using its class name. However, if you wish to bind it under a different name, you can use $this->mockAs(...).

Usage:

$mock = $this->mockAs(MyService::class, 'acme.service', function (MockInterface $mock) {
    /* ... */
});

Anything that resolves acme.service from the container will now receive your mocked service.

Mock Config

For packages that ship with a configuration file, and still want to unit test their services, you can use $this->mockConfig(...) to bind a basic configuration repository to the container that yields the provided values.

Usage:

$this->mockConfig([
    'my-package' => [
        'setting-1' => 'foo'
    ]
]);

config('my-package.setting-1'); // "foo"

4. Foundation Helpers

There are some helper methods that ship with the Laravel Framework that help you interact with the service container. This package replicates a subset of those same methods, so that you get the same quality of life, but without having to include the entire Laravel Framework in your package. Don't worry, if you do decide to use the entire Laravel Framework, Laravel's helpers will take precedence over these.

Assertions

Laravel's Test Case comes with some basic assertions that are useful in unit tests. These have also been included.

1. Array Subset

Asserts that an array has a specified subset.

Usage:

/** @test */
public function it_has_some_attributes(): void
{
    $myObject = $this->newMyObject();

    $this->assertArraySubset([
        'foo' => 'bar'
    ], $myObject->toArray());
}

Pest

If you're using Pest, this package provides some additional extensions.

1. Expectations

The assertions for PHPUnit offered by this package also have their own Pest expectation flavors:

  • PHPUnit => Pest
  • $this->assertArraySubset($subset, $actual) => expect($actual)->toContainArraySubset($subset)

2. Pest Helpers

If you're focused on package development, and you want to use Pest, be wary of the Laravel Pest Plugin, (pestphp/pest-plugin-laravel). This package requires the entire Laravel Framework, which isn't always the best approach for package development. Therefore, this package includes a subset of the helpers offered by the Laravel Pest Plugin, specifically those that interact with the service container.

  • swap($abstract, $instance)
  • instance($abstract, $instance)
  • mock($abstract, $mock = null)
  • mockAs($abstract, $alias, $mock = null)
  • partialMock($abstract, $mock = null)
  • spy($abstract, $mock = null)