---
title: "Testing — Credits for Laravel | Roundly"
description: "Credits::fake() records every add, deduct and set without writing; or test balances, the guard and events against a real database."
url: https://roundly-consulting.com/open-source/docs/credits-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Credits for Laravel](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)

# Testing

Credits::fake() swaps the manager with a recorder that writes no ledger row and fires no event, and returns it. The swap covers the facade, every injected CreditsManager, the HasCredits model methods and credits:modify:

```php
use RoundlyConsulting\Credits\Facades\Credits;

$fake = Credits::fake();

// ... run the code under test ...

$fake->assertAdded($user, 100, bucket: 'points');   // amount and bucket optional
$fake->assertDeducted($user, 30);                   // the positive amount, as passed to deduct()
$fake->assertSet($user, 0);                         // the requested target
$fake->assertNothingAdded();
$fake->assertNothingDeducted();
$fake->assertNothingSet();
$fake->assertNothingModified();                     // no add, deduct or set at all
```

- assertAdded($owner, ?$amount, ?$bucket) / assertNothingAdded() — grants, by owner and optionally amount and bucket.
- assertDeducted($owner, ?$amount, ?$bucket) / assertNothingDeducted() — deductions, by the positive amount as passed to deduct() (a PHP\_INT\_MIN deduction, whose size does not fit an int, matches only with $amount omitted).
- assertSet($owner, ?$amount, ?$bucket) / assertNothingSet() — setTo() calls, by the requested target.
- assertNothingModified() — no add, deduct or set at all.

The fake keeps an in-memory ledger. Balances add it to the owner’s real rows, so a grant made on the fake can be spent on the fake. The overdraft guard still applies: a deduction the real manager would refuse throws InsufficientCreditsException — with the same message and $minimum — and is not recorded. So does the 64-bit bound: a change whose resulting balance, or a setTo() whose delta, does not fit throws money’s AmountOverflow with the real ledger’s message and is not recorded, and a balance or total past int64 throws it too. A point in time is compared as an instant at second precision, like the real created\_at. setTo() is recorded even when the balance already matches.

## Against a real database

When the test is about the ledger itself, run against a real database with RefreshDatabase — the published migration runs like any other. Write through the public API and assert through it:

```php
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Credits\Events\CreditsModified;
use RoundlyConsulting\Credits\Exceptions\InsufficientCreditsException;

uses(RefreshDatabase::class);

it('grants and spends credits', function () {
    $user = User::factory()->create();

    $user->modifyCredits(100, 'signup bonus');
    $user->modifyCredits(-30, 'purchase', ['order_id' => 42]);

    expect($user->creditsBalance())->toBe(70)
        ->and($user->hasCredits(50))->toBeTrue()
        ->and($user->credits()->count())->toBe(2);
});

it('refuses to overdraw and writes nothing', function () {
    $user = User::factory()->create();
    $user->modifyCredits(20);

    expect(fn () => $user->modifyCredits(-50))
        ->toThrow(InsufficientCreditsException::class);

    expect($user->creditsBalance())->toBe(20)
        ->and($user->credits()->count())->toBe(1);
});

it('announces every change', function () {
    Event::fake([CreditsModified::class]);

    $user = User::factory()->create();
    $user->modifyCredits(10, 'welcome');

    Event::assertDispatched(
        CreditsModified::class,
        fn (CreditsModified $event) => $event->balance === 10 && $event->creditable->is($user),
    );
});
```

## Point-in-time balances

created\_at drives past balances, so Laravel’s time travel is all you need:

```php
it('reconstructs a past balance', function () {
    $user = User::factory()->create();

    $this->travelTo(now()->subWeek());
    $user->modifyCredits(100);
    $this->travelBack();

    $user->modifyCredits(-40);

    expect($user->creditsBalance(now()->subDays(3)))->toBe(100)
        ->and($user->creditsBalance())->toBe(60);
});
```

## Factory

Credit ships with a factory for seeding ledger rows directly. Attach it to an owner through the creditable relation:

```php
use RoundlyConsulting\Credits\Models\Credit;

// The factory picks a random amount (-1000..1000) unless you set one.
Credit::factory()->for($user, 'creditable')->create(['amount' => 250, 'bucket' => 'promotional']);
```

The factory builds the configured credits.model, so a subclass’s factory() returns the subclass, and a row without a bucket lands in credits.default\_bucket. Rows seeded through the factory bypass the overdraft guard and dispatch no CreditsModified event — use modifyCredits() when the test is about those.

## Engines

SQLite accepts a uuid in an integer column silently. If you changed key\_type or primary\_key\_type, run at least part of your suite on the engine you deploy to.

## The package’s own suite

To run the package’s test suite from a checkout:

```bash
composer test
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/credits-for-laravel.md)
