PHP code example of tobento / app-testing

1. Go to this page and download the library: Download tobento/app-testing library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

tobento / app-testing example snippets


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

final class SomeAppTest extends TestCase
{
    public function createApp(): AppInterface
    {
        return 

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;
    }
}

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');
    }
}

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();
    }
}

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');
    }
}

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']);

$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)

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);
    }
}

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');
    }
}

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="http://localhost/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'
);

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
    }
}

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"');
    }
}

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\TestCase;

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'api/user/1');
        
        // assertions:
        $http->response()
            ->assertStatus(200)
            ->assertJson([
                'id' => 1,
                'name' => 'John',
            ]);
    }
}

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

final class SomeAppTest extends TestCase
{
    public function testSomeRoute(): void
    {
        // faking:
        $http = $this->fakeHttp();
        $http->request('GET', 'api/users');
        
        // assertions:
        $http->response()
            ->assertJson(fn (AssertableJson $json) =>
                $json->has(items: 3)
                     ->has(key: '0', items: 2, value: AssertableJson $json) =>
                        $json->has(key: 'id', value: 1)
                             ->has(key: 'name', value: 'Tom')
                     )
            );
    }
}

$json->has(key: 'name');

// using dot notation:
$json->has(key: 'address.firstname');

$json->has(value: ['name' => 'Tom']);

$json->has(key: 'name', value: 'Tom');
$json->has(key: 'address.firstname', value: 'John');

$json->has(items: 3);

// with key
$json->has(key: 'colors', items: 3);

$json->has(passes: true);
$json->has(passes: false); // will fail

// with key
$json->has(key: 'color', passes: fn (mixed $color) => is_string($color));

$json->hasnt(key: 'name');
$json->hasnt(key: 'address.firstname');
$json->hasnt(value: ['name' => 'Tom']);
$json->hasnt(key: 'address.firstname', value: 'John');
$json->hasnt(items: 3);
$json->hasnt(key: 'colors', items: 3);
$json->hasnt(value: 'name', passes: fn (mixed $color) => is_string($color));

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();
    }
}

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

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

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: '[email protected]');
        // or:
        $user = $auth->getUserRepository()->findByIdentity(email: '[email protected]');
        
        $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: '[email protected]');
        
        $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();
    }
}

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

class FileStorageTest extends \Tobento\App\Testing\TestCase
{
    // you may refresh all file storages after each test.
    use RefreshFileStorages;
    
    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)

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();
    }
}

use Tobento\Service\Queue\QueueInterface;

$fakeQueue->clearQueue(
    queue: $fakeQueue->queue(name: 'file') // QueueInterface
);

$fakeQueue->runJobs($fakeQueue->queue(name: 'sync')->getAllJobs());

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);
    }
}

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('[email protected]')
                    ->to(new Address('[email protected]', '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('[email protected]', 'Name')
            ->assertHasTo('[email protected]', 'Name')
            ->assertHasCc('[email protected]', 'Name')
            ->assertHasBcc('[email protected]', 'Name')
            ->assertReplyTo('[email protected]', 'Name')
            ->assertSubject('Subject')
            ->assertTextContains('Lorem')
            ->assertHtmlContains('Lorem')
            ->assertIsQueued()
            ->assertHasParameter(
                Parameter\File::class,
                fn (Parameter\File $f) => $f->file()->getBasename() === 'image.jpg'
            )
            ->assertTimes(1);
    }
}

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: '[email protected]',
                    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() === '[email protected]';
            })
            // 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
    {
        // ...
    }
}

use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Tobento\App\AppInterface;
use Tobento\App\Testing\Logging\LogEntry;
use Tobento\Service\Routing\RouterInterface;

class LoggingTest 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\Logging\Boot\Logging::class);
        
        // routes: just for demo, normally done with a boot!
        $app->on(RouterInterface::class, static function(RouterInterface $router): void {
            $router->post('login', function (ServerRequestInterface $request, LoggerInterface $logger) {    
                
                $logger->info('User logged in.', ['user_id' => 3]);
                
                return 'response';
            });
        });
        
        return $app;
    }

    public function testIsLogged()
    {
        // fakes:
        $fakeLogging = $this->fakeLogging();
        $http = $this->fakeHttp();
        $http->request(method: 'POST', uri: 'login');
        
        // run the app:
        $this->runApp();
        
        // assertions using default logger:
        $fakeLogging->logger()
            ->assertLogged(fn (LogEntry $log): bool =>
                $log->level === 'info'
                && $log->message === 'User logged in.' 
                && $log->context === ['user_id' => 3]
            )
            ->assertNotLogged(
                fn (LogEntry $log): bool => $log->level === 'error'
            )
            ->assertLoggedTimes(
                fn (LogEntry $log): bool => $log->level === 'info',
                1
            );
        
        // specific logger:
        $fakeLogging->logger(name: 'error')
            ->assertNothingLogged();
    }
}