tobento/app-testing

1.0.12 2024-09-28 10:08 UTC

This package is auto-updated.

Last update: 2024-09-28 10:09:25 UTC


README

应用测试支持。

目录

入门

运行以下命令添加应用测试项目的最新版本。

composer require tobento/app-testing

需求

  • PHP 8.0 或更高版本

文档

入门

要测试您的应用程序,请扩展 Tobento\App\Testing\TestCase 类。

接下来,使用 createApp 方法创建用于测试的应用程序

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;

final class SomeAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        return require __DIR__.'/../app/app.php';
    }
}

临时应用

您可以使用 createTmpApp 方法创建一个仅用于测试单个引导的应用程序。

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;

final class SomeAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(SomeRoutes::class);
        // ...
        return $app;
    }
}

最后,使用可用的模拟器编写测试。

默认情况下,应用程序尚未启动或运行。您需要在每个测试方法中执行此操作。尽管如此,某些模拟器方法会自动运行应用程序,例如 fakeHttp 的 response 方法。

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'foo/bar');
        
        // interact with the app:
        $app = $this->getApp();
        $app->booting();
        $app->run();
        // or
        $app = $this->bootingApp();
        $app->run();
        // or
        $app = $this->runApp();
        
        // assertions:
        $http->response()
            ->assertStatus(200)
            ->assertBodySame('foo');
    }
}

应用程序清理

在创建临时应用程序时,您可以调用 deleteAppDirectory 方法删除应用程序目录。

final class SomeAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(SomeRoutes::class);
        // ...
        return $app;
    }
    
    public function testSomeRoute(): void
    {
        // testing...
        
        $this->deleteAppDirectory();
    }
}

或者您可以使用 tearDown 方法

final class SomeAppTest extends TestCase
{
    protected function tearDown(): void
    {
        parent::tearDown();
        
        $this->deleteAppDirectory();
    }
}

配置测试

在某些情况下,您可能需要为特定测试定义或替换配置值

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomething(): void
    {
        // faking:
        $config = $this->fakeConfig();
        $config->with('app.environment', 'testing');
        
        $this->runApp();
        
        // assertions:
        $config
            ->assertExists(key: 'app.environment')
            ->assertSame(key: 'app.environment', value: 'testing');
    }
}

HTTP 测试

请求和响应

如果您已安装了 App Http 扩展包,则可以使用 fakeHttp 方法测试您的应用程序。

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/1');
        
        // you may interact with the app:
        $app = $this->getApp();
        $app->booting();
        
        // assertions:
        $http->response()
            ->assertStatus(200)
            ->assertBodySame('foo');
    }
}

请求方法

$http = $this->fakeHttp();
$http->request(
    method: 'GET',
    uri: 'foo/bar',
    server: [],
    query: ['sort' => 'desc'],
    headers: ['Content-type' => 'application/json'],
    cookies: ['token' => 'xxxxxxx'],
    files: ['profile' => ...],
    body: ['foo' => 'bar'],
);

或者您可能更喜欢使用以下方法

$http = $this->fakeHttp();
$http->request(method: 'GET', uri: 'foo/bar', server: [])
    ->query(['sort' => 'desc'])
    ->headers(['Content-type' => 'application/json'])
    ->cookies(['token' => 'xxxxxxx'])
    ->files(['profile' => ...])
    ->body(['foo' => 'bar']);

使用 json 方法创建具有以下头部的请求

  • Accept: application/json
  • Content-type: application/json
$http = $this->fakeHttp();
$http->request('POST', 'foo/bar')->json(['foo' => 'bar']);

响应方法

调用响应方法将自动运行应用程序。

use Psr\Http\Message\ResponseInterface;

$http->response()
    ->assertStatus(200)
    ->assertBodySame('foo')
    ->assertBodyNotSame('bar')
    ->assertBodyContains('foo')
    ->assertBodyNotContains('bar')
    ->assertContentType('application/json')
    ->assertHasHeader(name: 'Content-type')
    ->assertHasHeader(name: 'Content-type', value: 'application/json') // with value
    ->assertHeaderMissing(name: 'Content-type')
    ->assertCookieExists(key: 'token')
    ->assertCookieMissed(key: 'token')
    ->assertCookieSame(key: 'token', value: 'value')
    ->assertHasSession(key: 'key')
    ->assertHasSession(key: 'key', value: 'value') // with value
    ->assertSessionMissing(key: 'key')
    ->assertLocation(uri: 'uri')
    ->assertRedirectToRoute(name: 'route', parameters: []);

// you may get the response:
$response = $http->response()->response();
var_dump($response instanceof ResponseInterface::class);
// bool(true)

withoutMiddleware

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->withoutMiddleware(Middleware::class, AnotherMiddleware::class);
        $http->request('GET', 'user/1');
        
        // assertions:
        $http->response()->assertStatus(200);
    }
}

previousUri

如果您控制器使用 previous uri 在发生错误时进行重定向,则可以设置 previous uri。

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->previousUri('users/create');
        $http->request('POST', 'users');
        
        // Or after booting using a named route:
        $app = $this->bootingApp();
        $http->previousUri($app->routeUrl('users.create'));
        
        // assertions:
        $http->response()
            ->assertStatus(301)
            ->assertLocation(uri: 'users/create');
    }
}

HTTP URL

有时您可能希望修改 HTTP URL 以获得相对 URL,例如;

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $config = $this->fakeConfig();
        $config->with('http.url', ''); // modify
        
        $http = $this->fakeHttp();
        $http->request('GET', 'orders');
        
        // assertions:
        $http->response()
            // if modified:
            ->assertNodeExists('a[href="orders/5"]')
            
            // if not modified:
            ->assertNodeExists('a[href="https:///orders/5"]');
    }
}

后续请求

在发出请求后,后续请求将创建一个新的应用程序。第一个请求中的任何模拟器都将重新启动。

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/1');
        
        // assertions:
        $http->response()->assertStatus(200);
        
        // subsequent request:
        $http->request('GET', 'user/2');
        
        // assertions:
        $http->response()->assertStatus(200);
    }
}

跟随重定向

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'login');
        $auth = $this->fakeAuth();
        
        // you may interact with the app:
        $app = $this->bootingApp();
        $user = $auth->getUserRepository()->create(['username' => 'tom']);
        $auth->authenticatedAs($user);
        
        // assertions:
        $http->response()->assertStatus(302);
        $auth->assertAuthenticated();
        
        // following redirects:
        $http->followRedirects()->assertStatus(200);
        
        // fakers others than http, must be recalled.
        $this->fakeAuth()->assertAuthenticated();
        // $auth->assertAuthenticated(); // would be from previous request
    }
}

文件上传

您可以使用文件工厂生成用于测试目的的虚拟文件或图像。

use Tobento\App\Testing\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request(
            method: 'GET',
            uri: 'user/1',
            files: [
                // Create a fake image 640x480
                'profile' => $http->getFileFactory()->createImage('profile.jpg', 640, 480),
            ],
        );
        
        // assertions:
        $http->response()->assertStatus(200);
    }
}

创建一个虚拟图像

$image = $http->getFileFactory()->createImage(
    filename: 'profile.jpg', 
    width: 640, 
    height: 480
);

创建一个虚拟文件

$file = $http->getFileFactory()->createFile(
    filename: 'foo.txt'
);

// Create a file with size - 100kb
$file = $http->getFileFactory()->createFile(
    filename: 'foo.txt',
    kilobytes: 100
);

// Create a file with size - 100kb
$file = $http->getFileFactory()->createFile(
    filename: 'foo.txt',
    mimeType: 'text/plain'
);

$file = $http->getFileFactory()->createFileWithContent(
    filename: 'foo.txt', 
    content: 'Hello world',
    mimeType: 'text/plain'
);

爬取响应内容

您可以使用 Symfony Dom Crawler 爬取响应内容。

use Tobento\App\Testing\TestCase;
use Symfony\Component\DomCrawler\Crawler;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/comments');
        
        // assertions:
        $response = $http->response()->assertStatus(200);
        
        $this->assertCount(4, $response->crawl()->filter('.comment'));
        
        // returns the crawler:
        $crawler = $response()->crawl(); // Crawler
    }
}

assertNodeExists

use Tobento\App\Testing\TestCase;
use Symfony\Component\DomCrawler\Crawler;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/comments');
        
        // assertions:
        $http->response()
            ->assertStatus(200)
            // Assert if a node exists:
            ->assertNodeExists('a[href="https://example.com"]')
            // Assert if a node exists based on a truth-test callback:
            ->assertNodeExists('h1', fn (Crawler $n): bool => $n->text() === 'Comments')
            // Assert if a node exists based on a truth-test callback:
            ->assertNodeExists('ul', static function (Crawler $n) {
                return $n->children()->count() === 2
                    && $n->children()->first()->text() === 'foo';
            }, 'There first ul child has no text "foo"');
    }
}

assertNodeMissing

use Tobento\App\Testing\TestCase;
use Symfony\Component\DomCrawler\Crawler;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/comments');
        
        // assertions:
        $http->response()
            ->assertStatus(200)
            // Assert if a node is missing:
            ->assertNodeMissing('h1')
            // Assert if a node is missing based on a truth-test callback:
            ->assertNodeMissing('p', static function (Crawler $n) {
                return $n->attr('class') === 'error'
                    && $n->text() === 'Error Message';
            }, 'An unexpected error message was found');
    }
}

示例表单爬取

use Tobento\App\Testing\TestCase;
use Symfony\Component\DomCrawler\Crawler;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/comments');
        
        // assertions:
        $response = $http->response()->assertStatus(200);
        
        $form = $response->crawl(
            // you may pass a base uri or base href:
            uri: 'http://www.example.com',
            baseHref: null,
        )->selectButton('My super button')->form();
        
        $this->assertSame('POST', $form->getMethod());
    }
}

响应宏

您可能希望使用宏将方便的辅助程序添加到测试响应中。

use Tobento\App\Testing\Http\TestResponse;

final class SomeAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        // ...
        
        // we may add the macro here:
        TestResponse::macro('assertOk', function(): static {
            $this->assertStatus(200);                
            return $this;
        });

        return $app;
    }
    
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'user/comments');
        
        // assertions:
        $http->response()->assertOk();
    }
}

刷新会话

您可以使用 RefreshSession 特性在每次测试后刷新您的会话

use Tobento\App\Testing\TestCase;
use Tobento\App\Testing\Http\RefreshSession;

final class SomeAppTest extends TestCase
{
    use RefreshSession;
    
    public function testSomething(): void
    {
        // ...
    }
}

认证测试

如果您已安装了 App User 扩展包,则可以使用 fakeAuth 方法测试您的应用程序。

以下两个示例假设您已经以某种方式在测试中播种了用户

use Tobento\App\Testing\TestCase;
use Tobento\App\User\UserRepositoryInterface;

final class SomeAppTest extends TestCase
{
    public function testSomeRouteWhileAuthenticated(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'profile');
        $auth = $this->fakeAuth();
        
        // you may change the token storage:
        //$auth->tokenStorage('inmemory');
        //$auth->tokenStorage('session');
        //$auth->tokenStorage('repository');
        
        // boot the app:
        $app = $this->bootingApp();
        
        // authenticate user:
        $userRepo = $app->get(UserRepositoryInterface::class);
        $user = $userRepo->findByIdentity(email: 'foo@example.com');
        // or:
        $user = $auth->getUserRepository()->findByIdentity(email: 'foo@example.com');
        
        $auth->authenticatedAs($user);
        
        // assertions:
        $http->response()->assertStatus(200);
        $auth->assertAuthenticated();
        // or:
        //$auth->assertNotAuthenticated();
    }
}

带令牌的示例

您可能希望通过创建令牌来验证用户

use Tobento\App\Testing\TestCase;
use Tobento\App\User\UserRepositoryInterface;

final class SomeAppTest extends TestCase
{
    public function testSomeRouteWhileAuthenticated(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'profile');
        $auth = $this->fakeAuth();
        
        // boot the app:
        $app = $this->bootingApp();
        
        // authenticate user:
        $user = $auth->getUserRepository()->findByIdentity(email: 'foo@example.com');
        
        $token = $auth->getTokenStorage()->createToken(
            payload: ['userId' => $user->id(), 'passwordHash' => $user->password()],
            authenticatedVia: 'loginform',
            authenticatedBy: 'testing',
            //issuedAt: $issuedAt,
            //expiresAt: $expiresAt,
        );
        
        $auth->authenticatedAs($token);
        
        // assertions:
        $http->response()->assertStatus(200);
        $auth->assertAuthenticated();
    }
}

播种用户示例

这是为测试用户进行播种的一种可能方法。您也可以通过创建和使用播种器来播种用户。

use Tobento\App\Testing\TestCase;
use Tobento\App\Testing\Database\RefreshDatabases;
use Tobento\App\User\UserRepositoryInterface;
use Tobento\App\User\AddressRepositoryInterface;
use Tobento\App\Seeding\User\UserFactory;

final class SomeAppTest extends TestCase
{
    use RefreshDatabases;

    public function testSomeRouteWhileAuthenticated(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'profile');
        $auth = $this->fakeAuth();
        
        // boot the app:
        $app = $this->bootingApp();
        
        // Create a user:
        $user = $auth->getUserRepository()->create(['username' => 'tom']);
        // or using the user factory:
        $user = UserFactory::new()->withUsername('tom')->withPassword('123456')->createOne();
        
        // authenticate user:
        $auth->authenticatedAs($user);
        
        // assertions:
        $http->response()->assertStatus(200);
        $auth->assertAuthenticated();
    }
}

您可以查看用户播种以了解关于UserFactory::class的更多信息。

文件存储测试

如果您已安装应用文件存储包,您可以使用fakeFileStorage方法测试您的应用程序,该方法允许您创建一个模拟真实存储行为的假存储,但实际上不会将任何文件发送到云端。这样,您可以测试文件上传而不用担心意外将真实文件发送到云端。

使用临时应用的示例

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouterInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tobento\Service\FileStorage\StoragesInterface;
use Tobento\Service\FileStorage\Visibility;

class FileStorageTest extends \Tobento\App\Testing\TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(\Tobento\App\FileStorage\Boot\FileStorage::class);
        
        // routes: just for demo, normally done with a boot!
        $app->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('upload', function (ServerRequestInterface $request, StoragesInterface $storages) {    
                
                $file = $request->getUploadedFiles()['profile'];
                $storage = $storages->get('uploads');
                
                $storage->write(
                    path: $file->getClientFilename(),
                    content: $file->getStream()
                );
                
                $storage->copy(from: $file->getClientFilename(), to: 'copy/'.$file->getClientFilename());
                $storage->move(from: 'copy/'.$file->getClientFilename(), to: 'move/'.$file->getClientFilename());
                $storage->createFolder('foo/bar');
                $storage->setVisibility('foo/bar', Visibility::PRIVATE);
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testFileUpload()
    {
        // fakes:
        $fileStorage = $this->fakeFileStorage();
        $http = $this->fakeHttp();
        $http->request(
            method: 'POST',
            uri: 'upload',
            files: [
                // Create a fake image 640x480
                'profile' => $http->getFileFactory()->createImage('profile.jpg', 640, 480),
            ],
        );
        
        // run the app:
        $this->runApp();
        
        // assertions:
        $fileStorage->storage(name: 'uploads')
            ->assertCreated('profile.jpg')
            ->assertNotCreated('foo.jpg')
            ->assertExists('profile.jpg')
            ->assertNotExist('foo.jpg')
            ->assertCopied(from: 'profile.jpg', to: 'copy/profile.jpg')
            ->assertNotCopied(from: 'foo.jpg', to: 'copy/foo.jpg')
            ->assertMoved(from: 'copy/profile.jpg', to: 'move/profile.jpg')
            ->assertNotMoved(from: 'foo.jpg', to: 'copy/foo.jpg')
            ->assertFolderCreated('foo/bar')
            ->assertFolderNotCreated('baz')
            ->assertFolderExists('foo/bar')
            ->assertFolderNotExist('baz')
            ->assertVisibilityChanged('foo/bar');
    }
}

存储方法

$fileStorage = $this->fakeFileStorage();

// Get default storage:
$defaultStorage = $fileStorage->storage();

// Get specific storage:
$storage = $fileStorage->storage(name: 'uploads');

存储方法

use Tobento\Service\FileStorage\StoragesInterface;

$fileStorage = $this->fakeFileStorage();

// Get the storages:
$storages = $fileStorage->storages();

var_dump($storages instanceof StoragesInterface);
// bool(true)

队列测试

如果您已安装应用队列包,您可以使用fakeQueue方法测试您的应用程序,该方法允许您创建一个假队列以防止作业被发送到实际队列。

使用临时应用的示例

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouterInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tobento\Service\Queue\QueueInterface;
use Tobento\Service\Queue\JobInterface;
use Tobento\Service\Queue\Job;

class QueueTest extends \Tobento\App\Testing\TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(\Tobento\App\Queue\Boot\Queue::class);
        
        // routes: just for demo, normally done with a boot!
        $app->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('queue', function (ServerRequestInterface $request, QueueInterface $queue) {    

                $queue->push(new Job(
                    name: 'sample',
                    payload: ['key' => 'value'],
                ));            
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testIsQueued()
    {
        // fakes:
        $fakeQueue = $this->fakeQueue();
        $http = $this->fakeHttp();
        $http->request(method: 'POST', uri: 'queue');
        
        // run the app:
        $this->runApp();
        
        // assertions:
        $fakeQueue->queue(name: 'sync')
            ->assertPushed('sample')
            ->assertPushed('sample', function (JobInterface $job): bool {
                return $job->getPayload()['key'] === 'value';
            })
            ->assertNotPushed('sample:foo')
            ->assertNotPushed('sample', function (JobInterface $job): bool {
                return $job->getPayload()['key'] === 'invalid';
            })
            ->assertPushedTimes('sample', 1);
        
        $fakeQueue->queue(name: 'file')
            ->assertNothingPushed();
    }
}

事件测试

如果您已安装应用事件包,您可以使用fakeEvents方法测试您的应用程序,该方法记录所有已分发的所有事件,并提供断言方法,您可以使用这些方法来检查是否已分发了特定事件以及它们分发了多少次。目前,只有默认事件将被记录。《特定事件》尚不支持。

使用临时应用的示例

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouterInterface;
use Tobento\Service\Event\EventsInterface;
use Psr\Http\Message\ServerRequestInterface;

class QueueTest extends \Tobento\App\Testing\TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(\Tobento\App\Event\Boot\Event::class);
        
        // routes: just for demo, normally done with a boot!
        $app->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('registration', function (ServerRequestInterface $request, EventsInterface $events) {    

                $events->dispatch(new UserRegistered(username: 'tom'));
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testDispatchesEvent()
    {
        // fakes:
        $events = $this->fakeEvents();
        $http = $this->fakeHttp();
        $http->request(method: 'POST', uri: 'registration');
        
        // run the app:
        $this->runApp();
        
        // assertions:
        $events
            // Assert if an event dispatched one or more times:
            ->assertDispatched(UserRegistered::class)
            // Assert if an event dispatched one or more times based on a truth-test callback:
            ->assertDispatched(UserRegistered::class, static function(UserRegistered $event): bool {
                return $event->username === 'tom';
            })
            // Asserting if an event were dispatched a specific number of times:
            ->assertDispatchedTimes(UserRegistered::class, 1)
            // Asserting an event were not dispatched:
            ->assertNotDispatched(FooEvent::class)
            // Asserting an event were not dispatched based on a truth-test callback:
            ->assertNotDispatched(UserRegistered::class, static function(UserRegistered $event): bool {
                return $event->username !== 'tom';
            })
            // Assert if an event has a listener attached to it:
            ->assertListening(UserRegistered::class, SomeListener::class);
    }
}

邮件测试

如果您已安装应用邮件包,您可以使用fakeMail方法测试您的应用程序,该方法允许您创建一个假邮件发送器以防止消息被发送。

使用临时应用的示例

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouterInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tobento\Service\Mail\MailerInterface;
use Tobento\Service\Mail\Message;
use Tobento\Service\Mail\Address;
use Tobento\Service\Mail\Parameter;

class MailTest extends \Tobento\App\Testing\TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(\Tobento\App\View\Boot\View::class); // to support message templates
        $app->boot(\Tobento\App\Mail\Boot\Mail::class);
        
        // routes: just for demo, normally done with a boot!
        $this->getApp()->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('mail', function (ServerRequestInterface $request, MailerInterface $mailer) {
                
                $message = (new Message())
                    ->from('from@example.com')
                    ->to(new Address('to@example.com', 'Name'))
                    ->subject('Subject')
                    ->html('<p>Lorem Ipsum</p>');

                $mailer->send($message);
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testMessageMailed()
    {
        // fakes:
        $fakeMail = $this->fakeMail();
        $http = $this->fakeHttp();
        $http->request(method: 'POST', uri: 'mail');
        
        // run the app:
        $this->runApp();
        
        // assertions:
        $fakeMail->mailer(name: 'default')
            ->sent(Message::class)
            ->assertFrom('from@example.com', 'Name')
            ->assertHasTo('to@example.com', 'Name')
            ->assertHasCc('cc@example.com', 'Name')
            ->assertHasBcc('bcc@example.com', 'Name')
            ->assertReplyTo('replyTo@example.com', 'Name')
            ->assertSubject('Subject')
            ->assertTextContains('Lorem')
            ->assertHtmlContains('Lorem')
            ->assertIsQueued()
            ->assertHasParameter(
                Parameter\File::class,
                fn (Parameter\File $f) => $f->file()->getBasename() === 'image.jpg'
            )
            ->assertTimes(1);
    }
}

通知器测试

如果您已安装应用通知包,您可以使用fakeNotifier方法测试您的应用程序,该方法允许您创建一个假通知发送器以防止通知消息被发送。

使用临时应用的示例

use Tobento\App\AppInterface;
use Tobento\Service\Routing\RouterInterface;
use Psr\Http\Message\ServerRequestInterface;
use Tobento\Service\Notifier\NotifierInterface;
use Tobento\Service\Notifier\ChannelMessagesInterface;
use Tobento\Service\Notifier\Notification;
use Tobento\Service\Notifier\Recipient;

class NotifierTest extends \Tobento\App\Testing\TestCase
{
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..');
        $app->boot(\Tobento\App\Http\Boot\Routing::class);
        $app->boot(\Tobento\App\Notifier\Boot\Notifier::class);
        
        // routes: just for demo, normally done with a boot!
        $this->getApp()->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('notify', function (ServerRequestInterface $request, NotifierInterface $notifier) {
                
                $notification = new Notification(
                    subject: 'New Invoice',
                    content: 'You got a new invoice for 15 EUR.',
                    channels: ['mail', 'sms', 'storage'],
                );

                // The receiver of the notification:
                $recipient = new Recipient(
                    email: 'mail@example.com',
                    phone: '15556666666',
                    id: 5,
                );

                $notifier->send($notification, $recipient);
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testNotified()
    {
        // fakes:
        $notifier = $this->fakeNotifier();
        $http = $this->fakeHttp();
        $http->request(method: 'POST', uri: 'notify');
        
        // run the app:
        $this->runApp();
        
        // assertions:
        $notifier
            // Assert if a notification is sent one or more times:
            ->assertSent(Notification::class)
            // Assert if a notification is sent one or more times based on a truth-test callback:
            ->assertSent(Notification::class, static function(ChannelMessagesInterface $messages): bool {
                $notification = $messages->notification();
                $recipient = $messages->recipient();
                
                // you may test the sent messages
                $mail = $messages->get('mail')->message();
                $this->assertSame('New Invoice', $mail->getSubject());

                return $notification->getSubject() === 'New Invoice'
                    && $messages->successful()->channelNames() === ['mail', 'sms', 'storage']
                    && $messages->get('sms')->message()->getTo()->phone() === '15556666666'
                    && $recipient->getAddressForChannel('mail', $notification)?->email() === 'mail@example.com';
            })
            // Asserting if a notification were sent a specific number of times:
            ->assertSentTimes(Notification::class, 1)
            // Asserting a notification were not sent:
            ->assertNotSent(Notification::class)
            // Asserting a notification were not sent based on a truth-test callback:
            ->assertNotSent(Notification::class, static function(ChannelMessagesInterface $messages): bool {
                $notification = $messages->notification();
                return $notification->getSubject() === 'New Invoice';
            })
            // Asserting that no notifications were sent:
            ->assertNothingSent();
    }
}

数据库测试

如果您已安装应用数据库包,您可以与您的数据库交互。

重置数据库

重置数据库有两种策略

刷新策略

此策略在每个测试后清理您的数据库。

use Tobento\App\Testing\TestCase;
use Tobento\App\Testing\Database\RefreshDatabases;

final class SomeAppTest extends TestCase
{
    use RefreshDatabases;
    
    public function testSomething(): void
    {
        // ...
    }
}

迁移策略

此策略在每个测试后使用迁移来清理您的数据库。

use Tobento\App\Testing\TestCase;
use Tobento\App\Testing\Database\MigrateDatabases;

final class SomeAppTest extends TestCase
{
    use MigrateDatabases;
    
    public function testSomething(): void
    {
        // ...
    }
}

替换数据库

您可以将数据库替换为测试不同的数据库。

示例替换默认存储数据库

use Tobento\App\Testing\TestCase;
use Tobento\App\AppInterface;
use Tobento\App\Testing\Database\RefreshDatabases;
use Tobento\Service\Database\DatabasesInterface;
use Tobento\Service\Database\DatabaseInterface;
use Tobento\Service\Database\PdoDatabase;

final class SomeAppTest extends TestCase
{
    use RefreshDatabases;
    
    public function createApp(): AppInterface
    {
        $app = $this->createTmpApp(rootDir: __DIR__.'/..', folder: 'app-mysql');
        $app->boot(\Tobento\App\User\Boot\User::class);
        
        // example changing databases:
        $app->on(DatabasesInterface::class, static function (DatabasesInterface $databases) {
            // change default storage database:
            $databases->addDefault('storage', 'mysql-storage');
            
            // you may change the mysql database:
            $databases->register(
                'mysql',
                function(string $name): DatabaseInterface {
                    return new PdoDatabase(
                        new \PDO(
                            dsn: 'mysql:host=localhost;dbname=app_testing',
                            username: 'root',
                            password: '',
                        ),
                        $name
                    );
                }
            );
        });
        
        return $app;
    }
    
    public function testSomething(): void
    {
        // ...
    }
}

鸣谢