Pest from scratch - Notes

After learning a few videos from this, this is maybe a version of a framework for testing because it's working on top of PHPUnit, and it looks so good and so much better than PHPUnit as the default, wait till you try the parallel.

https://pestphp.com/docs/installation

git clone <https://github.com/inertiajs/pingcrm.git>
composer require pestphp/pest-plugin-laravel --dev
npm install
npm run dev
//FROM THIS
<?php

namespace Tests\\Unit;

use PHPUnit\\Framework\\TestCase;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function test_example()
    {
        $this->assertTrue(true);
    }
}

//TO THIS
test('example', function() {
    $this->assertTrue(true);
});

Hooks

beforeAll(fn() => var_dump('Before All'));
beforeEach(fn() => var_dump('Before Each'));
afterEach(fn() => var_dump('Before Each'));
afterAll(fn() => var_dump('After All'));
// SetUp()
protected function setUp(): void
    {
        parent::setUp();

        $this->user = User::factory()->create([
            'account_id' => Account::create(['name' => 'Acme Corporation'])->id,
            'first_name' => 'John',
            'last_name' => 'Doe',
            'email' => 'johndoe@example.com',
            'owner' => true,
        ]);

        $organization = $this->user->account->organizations()->create(['name' => 'Example Organization Inc.']);

        $this->user->account->contacts()->createMany([
            [
                'organization_id' => $organization->id,
                'first_name' => 'Martin',
                'last_name' => 'Abbott',
                'email' => 'martin.abbott@example.com',
                'phone' => '555-111-2222',
                'address' => '330 Glenda Shore',
                'city' => 'Murphyland',
                'region' => 'Tennessee',
                'country' => 'US',
                'postal_code' => '57851',
            ], [
                'organization_id' => $organization->id,
                'first_name' => 'Lynn',
                'last_name' => 'Kub',
                'email' => 'lynn.kub@example.com',
                'phone' => '555-333-4444',
                'address' => '199 Connelly Turnpike',
                'city' => 'Woodstock',
                'region' => 'Colorado',
                'country' => 'US',
                'postal_code' => '11623',
            ],
        ]);
    }

// TO

beforeEach(function() {
    $this->user = User::factory()->create([
        'account_id' => Account::create(['name' => 'Acme Corporation'])->id,
        'first_name' => 'John',
        'last_name' => 'Doe',
        'email' => 'johndoe@example.com',
        'owner' => true,
    ]);

    $organization = $this->user->account->organizations()->create(['name' => 'Example Organization Inc.']);

    $this->user->account->contacts()->createMany([
        [
            'organization_id' => $organization->id,
            'first_name' => 'Martin',
            'last_name' => 'Abbott',
            'email' => 'martin.abbott@example.com',
            'phone' => '555-111-2222',
            'address' => '330 Glenda Shore',
            'city' => 'Murphyland',
            'region' => 'Tennessee',
            'country' => 'US',
            'postal_code' => '57851',
        ], [
            'organization_id' => $organization->id,
            'first_name' => 'Lynn',
            'last_name' => 'Kub',
            'email' => 'lynn.kub@example.com',
            'phone' => '555-333-4444',
            'address' => '199 Connelly Turnpike',
            'city' => 'Woodstock',
            'region' => 'Colorado',
            'country' => 'US',
            'postal_code' => '11623',
        ],
    ]);
});

To use refresh database

use Illuminate\\Foundation\\Testing\\RefreshDatabase;

uses(RefreshDatabase::class);

//OR go to TestCase
use Illuminate\\Foundation\\Testing\\LazilyRefreshDatabase;

use LazilyRefreshDatabase;

public function test_can_view_contacts()
{
	...
}

test('can view contacts', function() {
	...
});

Pest faker

composer require pestphp/pest-plugin-faker --dev
use App\\Models\\Contact;
use function Pest\\Faker\\faker;

it('can store a contact', function() {
    login()->post('/contacts', [
        'first_name'=> faker()->firstName, 
        'last_name'=> faker()->lastName,
        'email'=> faker()->email,
        'phone'=> faker()->e164PhoneNumber,
        'address'=> 'Jatayu no 35',
        'city'=> 'TesterField',
        'region'=> 'Doe',
        'country'=> faker()->randomElement(['us', 'ca']),
        'postal_code'=>faker()->postcode,
    ])->assertRedirect('/contacts')->assertSessionHas('success','Contact created.');

    expect(Contact::latest()->first())
        ->first_name->toBeString()->not->toBeEmpty()
        ->last_name->toBeString()->not->toBeEmpty()
        ->email->toBeString()->toBeString()->toContain('@')
        ->phone->toBePhoneNumber()
        ->city->toBe('TesterField')
        ->country->toBeIn(['us', 'ca']);
});

Custom

// pest.php
function login($user = null)
{
    return test()->actingAs($user ?? User::factory()->create());
}

// create own
expect()->extend('toBePhoneNumber', function () {
    expect($this->value)->toBeString()->toStartWith('+');
    if (strlen($this->value) < 6) {
        throw new ExpectationFailedException('Phone numbers must be at least 6 characters');
    }

    if (! is_numeric(\\Str::of($this->value)->after('+')->remove([' ', '-'])->__toString())) {
        throw new ExpectationFailedException('Phone number must be numeric');
    }
});

Data sets

it('can store a contact', function(array $data) {
    login()->post('/contacts', [... [
        'first_name'=> faker()->firstName, 
        'last_name'=> faker()->lastName,
        'email'=> faker()->email,
        'phone'=> faker()->e164PhoneNumber,
        'address'=> 'Jatayu no 35',
        'city'=> 'TesterField',
        'region'=> 'Doe',
        'country'=> faker()->randomElement(['us', 'ca']),
        'postal_code'=>faker()->postcode,
    ], ...$data])->assertRedirect('/contacts')->assertSessionHas('success','Contact created.');

    expect(Contact::latest()->first())
        ->first_name->toBeString()->not->toBeEmpty()
        ->last_name->toBeString()->not->toBeEmpty()
        ->email->toBeString()->toBeString()->toContain('@')
        ->phone->toBePhoneNumber()
        ->city->toBe('TesterField')
        ->country->toBeIn(['us', 'ca']);
})->with([
    'generic' => [[]],
    'email with spaces' => [[ 'email' => '"madindo"@mailinator.com']],
    '.co.uk with sharon' => [['email' => 'info@me.co.uk', 'first_name' => 'info']],
    'postcode with 25 char' => [['postal_code' => str_repeat('a', 25)]],
]);


php artisan pest:dataset Emails
dataset('valid emails', function () {
    return [
        'foo@bar.com',
        '"luke downing"@downing.tech',
        'you@me.co.uk',
        'arentdatasetcool@pest.com'
    ];
});‘˘
it('can store a contact', function($email) {
    ...
})->with('valid emails');

Groups

<?php
namespace App\\Rules;
use InvalidArgumentException;
use Illuminate\\Contracts\\Validation\\Rule;

class IsValidEmailAddress implements Rule
{
    public function passes($attribute, $value): bool 
    {
        if (! is_string($value)) {
            throw new InvalidArgumentException('The value must be a string');
        }
        return preg_match('/^\\S+@\\S+\\.\\S+$/', $value) > 0;
    }

    public function message() {

    }
}
php artisan test --group=laracasts

it('can validate an email', function() {
    $rule = new \\App\\Rules\\IsValidEmailAddress();
    expect($rule->passes('email', 'me@you.com'))->toBeTrue();
})->group('laracasts');

php artisan test --exclude=laracasts

or

uses()->group('laracasts');
it('can validate an email', function() {
    $rule = new \\App\\Rules\\IsValidEmailAddress();
    expect($rule->passes('email', 'me@you.com'))->toBeTrue();
});
it('throws an exception if the value is not a string', function() {
    $rule = new \\App\\Rules\\IsValidEmailAddress();
    expect($rule->passes('email', 1))->toBeTrue();
})->throws(InvalidArgumentException::class, 'The value must be a string')->group('current');
SKIP_TESTS=true php artisan test

it('throws an exception if the value is not a string', function() {
    $rule = new \\App\\Rules\\IsValidEmailAddress();
    expect($rule->passes('email', 1))->toBeTrue();
})->skip(getenv('SKIP_TESTS') ?? false, 'We no longer test exception')->throws(InvalidArgumentException::class, 'The value must be a string')->group('current');
it('throws an exception if the value is not a string', function() {
    $rule = new \\App\\Rules\\IsValidEmailAddress();
    expect($rule->passes('email', 1))->toBeTrue();
})
->skip(fn() => config('app.name') === 'foo', 'We no longer test exception')
->throws(InvalidArgumentException::class, 'The value must be a string')
->group('current');

Coverage and Parallel

XDEBUG_MODE=coverage php artisan test --coverage --min=80

https://pestphp.com/docs/plugins/parallel