Testing
Every facade has a fake. Calling fake() swaps the real implementation out of the
container for the rest of the test, so nothing reaches the network and you can assert on
what would have happened.
Tracking
use MatomoAnalytics\Facades\Matomo;
use MatomoAnalytics\Tracking\PageView;
$fake = Matomo::fake();
$this->get('/pricing');
$fake->assertTracked(PageView::class);
The fake records hits as the typed objects they are, so you assert on type rather than on a serialized payload. Pass a closure to inspect the hit:
use MatomoAnalytics\Tracking\Hit;
$fake->assertTracked(PageView::class, fn (Hit $hit): bool => $hit instanceof PageView && $hit->title === 'Pricing');
Type the parameter as Hit, not as the class in the first argument. The callback only ever
sees hits of the type you asked for, so a narrower type looks safe — and then breaks on
CustomParameters. A decorated hit matches by its INNER type, which is what makes
assertTracked(PageView::class, …) still find a page view somebody wrapped; but the callback
is handed the hit as it was tracked, which is the CustomParameters wrapper. A closure typed
fn (PageView $hit) throws a TypeError there.
Matomo::track(CustomParameters::for(new PageView('Pricing'))->dimension(1, 'plan:pro'));
// finds it, and hands the callback the CustomParameters wrapper
$fake->assertTracked(PageView::class, fn (Hit $hit): bool => true);
So the type is Hit and the narrowing happens inside the closure, with instanceof.
The hit types you can assert
$type is any class in MatomoAnalytics\\Tracking. Asserting an order should not mean
reading vendor/ for the class name, so here is the full set with the properties a callback
can read:
| Class | Properties |
|---|---|
PageView | title, url, serverTimeMs |
Event | category, action, name, value |
Goal | id, revenue |
Download | url |
Outlink | url |
Ping | — |
SiteSearch | keyword, category, count |
EcommerceOrder | orderId, grandTotal, items, subTotal, tax, shipping, discount |
EcommerceCartUpdate | grandTotal, items |
EcommerceView | sku, name, category, price, title, url |
ContentImpression | name, piece, target |
ContentInteraction | interaction, name, piece, target |
$fake->assertTracked(
EcommerceOrder::class,
fn (Hit $hit): bool => $hit instanceof EcommerceOrder && $hit->orderId === 'A-1001',
);
CustomParameters is a decorator rather than a hit type of its own — assert the inner type,
which is what it matches by.
The available assertions:
| Assertion | Asserts |
|---|---|
assertTracked($type, $callback) | A hit of this type was tracked, optionally matching the callback |
assertTrackedCount($count) | Exactly this many hits were tracked |
assertNothingTracked() | No hit was tracked at all |
assertAiChatbotTracked($callback) | An AI-chatbot telemetry hit was recorded |
assertNoAiChatbotTracked() | No AI-chatbot telemetry was recorded |
assertNothingTracked() is the one people forget, and it is the more valuable half of a
gating test: proving that an excluded visitor produces no hit is what actually tests
the gate.
The fake implements the full tracker contract, including the ecommerce and content methods, so it works with whatever your code calls.
Reporting
use MatomoAnalytics\Facades\MatomoReports;
$reports = MatomoReports::fake();
$reports->stub('VisitsSummary.get', ['nb_visits' => 42]);
// ... exercise code that calls MatomoReports ...
$reports->assertRequested('VisitsSummary.get');
stub() registers the response for a method; a method you did not stub returns null,
which is exactly what the real client does on failure — so your error path is testable
without simulating an outage:
$reports->setLastError('Matomo is unreachable');
assertRequested(), assertRequestedCount() and assertNothingRequested() mirror the
tracking assertions. Stubbing a null response is how you make one specific method fail
while others succeed.
The fake's query() returns a real query builder, so fluent code under test behaves the
same way and the request it produces is recorded like any other.
GDPR
use MatomoAnalytics\Facades\MatomoGdpr;
$gdpr = MatomoGdpr::fake()->stubFound([['idsite' => 1, 'idvisit' => 10]]);
// ... exercise code that calls MatomoGdpr::forget() ...
stubFound() sets what the lookup returns and stubDeleted() the deletion counts, so an
erasure flow can be asserted end to end. failMutations() makes forget() and export()
fail while the lookup still succeeds — the case worth testing, because that is when your
code has to decide what to tell the person who asked.
assertForgotten() takes an optional segment and callback; assertNothingForgotten() is
how you prove a confirmation step actually blocked the erasure.
Annotations
use MatomoAnalytics\Facades\MatomoAnnotations;
$annotations = MatomoAnnotations::fake();
// ... exercise your deploy hook ...
$annotations->assertAnnotated('Deployed 1.4.0');
fail() makes annotating fail, which is the assertion that matters for a deploy hook: a
failed annotation must not fail the deploy. assertNothingAnnotated() covers the
opted-out case — see release annotations.
Testing without fakes
Sometimes the fake is the wrong tool, because what you want to test is the real path.
Use sync mode. In sync mode the send happens inline, so by the time your assertion
runs the HTTP call has been made. Combine it with Laravel's HTTP fake to assert on the
actual wire payload:
use Illuminate\Support\Facades\Http;
use MatomoAnalytics\Facades\Matomo;
Http::fake();
config()->set([
'matomo-analytics.enabled' => true,
'matomo-analytics.host' => 'https://analytics.example.test',
'matomo-analytics.site_id' => 1,
'matomo-analytics.mode' => 'sync',
]);
Matomo::pageView('Pricing');
Http::assertSent(fn ($request): bool => $request['action_name'] === 'Pricing');
All four lines are needed. enabled is the master switch and ships off; without host and
site_id the gate refuses the hit as unconfigured, and Http::assertSent() then fails over a
request that was never meant to happen.
Leave the package unconfigured. With no MATOMO_HOST, tracking is a no-op. That is
the default state of a test suite, and it means a test that does not care about analytics
needs no setup at all.
Use the array buffer driver to test batch behavior without a database:
config()->set([
'matomo-analytics.mode' => 'batch',
'matomo-analytics.batch.driver' => 'array',
]);
Remember that batch escalation to the dead-letter queue counts consecutive failures in the
cache, so a test asserting that path needs a persistent cache store rather than the
array one — see reliability.