The package makes no HTTP calls, so there is nothing to fake. Point canonical.domain at a known host and assert on the string or tag that comes out.

tests/Feature/CanonicalTest.php
use Fkrzski\LaravelCanonical\Facades\Canonical;
beforeEach(function (): void {
config(['canonical.domain' => 'https://example.com']);
});
tests/Feature/CanonicalTest.php
it('generates a canonical URL for a path', function (): void {
expect(Canonical::generate('/products/item'))
->toBe('https://example.com/products/item');
});
it('normalises the root path to the bare domain', function (): void {
expect(Canonical::generate('/'))->toBe('https://example.com');
});

Bind a request into the container to exercise the no-argument path. The staging host proves the URL is built from the canonical domain, not the request:

tests/Feature/CanonicalTest.php
use Illuminate\Http\Request;
it('canonicalises the current request', function (): void {
$this->app->instance('request', Request::create('https://staging.example.com/blog/post'));
expect(Canonical::generate())->toBe('https://example.com/blog/post');
});

Blade::render() keeps the template’s surrounding whitespace, so trim before comparing:

tests/Feature/CanonicalComponentTest.php
use Illuminate\Support\Facades\Blade;
it('renders the canonical component', function (): void {
$html = trim(Blade::render('<x-canonical path="/products/item" />'));
expect($html)->toBe('<link rel="canonical" href="https://example.com/products/item" />');
});
it('renders the directive', function (): void {
$html = trim(Blade::render("@canonical('/products/item')"));
expect($html)->toBe('<link rel="canonical" href="https://example.com/products/item" />');
});

The domain is only validated on first use, so the assertion has to trigger one:

tests/Feature/CanonicalConfigTest.php
use Fkrzski\LaravelCanonical\Exceptions\CanonicalConfigurationException;
it('rejects a domain without a scheme', function (): void {
config(['canonical.domain' => 'example.com']);
expect(fn (): string => Canonical::generate('/page'))
->toThrow(CanonicalConfigurationException::class);
});
  1. Install the development dependencies.

    Terminal window
    composer install
  2. Run the full quality gate — Pint, Rector, PHPStan, type coverage, and a mutation-tested Pest run.

    Terminal window
    composer test
  3. Or just the unit suite while iterating.

    Terminal window
    composer test:unit