---
title: "Testing — Money for Laravel | Roundly"
description: "Exchange::fake() swaps in static rates and records refreshes, stores and prunes — assert every call, seed the rates table, format byte-exact."
url: https://roundly-consulting.com/open-source/docs/money-for-laravel/testing
language: en
---

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

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

# Testing

Exchange::fake($rates, $pivot) swaps in static rates — no HTTP, no cache — for the facade, an injected ExchangeManager and Money::convertTo(), and returns the fake to assert against:

```php
use RoundlyConsulting\Money\Facades\Exchange;
use RoundlyConsulting\Money\Money;

it('converts the order total to dollars', function () {
    $fake = Exchange::fake(['EUR/USD' => '1.0854']);

    $usd = Money::ofMajor('10', 'EUR')->convertTo('USD');

    expect($usd->equals(Money::ofMajor('10.85', 'USD')))->toBeTrue();
    $fake->assertRateRequested('EUR', 'USD');
});

it('never asks for a rate within one currency', function () {
    $fake = Exchange::fake();

    Money::ofMajor('10', 'EUR')->convertTo('EUR');

    $fake->assertNothingRequested();
});
```

Money::convertTo(), MoneyBag::total() and every converter resolved afterwards see the fake. Missing pairs are inverted or triangulated through the pivot; otherwise ExchangeRateUnavailable. The pivot defaults to the configured money.exchange.pivot, as the real drivers use; pass one to override it, or null for no triangulation. Rates can also be base-keyed: \['EUR' => \['USD' => '1.0854'\]\].

## The rates table is recorded, not written

Under the fake, Exchange::rates() records instead of fetching, writing, queueing or deleting — and so do the money:rates:refresh and money:rates:prune commands. source() and extend() keep the real behaviour, so a raw ecb source still downloads; pair it with Http::fake(). An unknown method throws BadMethodCallException.

The fake still refuses what the real store refuses, recording nothing: store() and manual() throw InvalidExchangeRate for a rate with no exact decimal of at most 40 characters (such as 1/3), and refresh() throws InvalidMoneyConfiguration for a driver that cannot be fetched from — database, config, chain or an unknown name — checked without fetching.

```php
use RoundlyConsulting\Money\Facades\Exchange;

it('stores the manual CZK rate', function () {
    $fake = Exchange::fake();

    Exchange::rates()->manual('EUR', 'CZK', '25.10');   // recorded — nothing is written

    $fake->assertStored('EUR', 'CZK', '25.10', 'manual');
    $fake->assertNothingRefreshed();
});
```

## Every assertion

```php
use RoundlyConsulting\Money\Enums\EcbFeed;

$fake = Exchange::fake(['EUR/USD' => '1.0854']);

// ... code that converts, refreshes, stores or prunes ...

$fake->assertRateRequested('EUR', 'USD');             // rate() was asked for the pair — conversions included
$fake->assertRefreshed('ecb', EcbFeed::Daily);        // rates()->refresh() ran — source and feed optional
$fake->assertRefreshQueued('ecb');                    // rates()->refreshLater() ran — source and feed optional
$fake->assertStored('EUR', 'CZK', '25.10', 'manual'); // store() or manual() got the pair — rate and source optional
$fake->assertPruned($before);                         // rates()->prune() ran — cut-off day optional

// each has a twin that passes when nothing happened
$fake->assertNothingRequested();
$fake->assertNothingRefreshed();
$fake->assertNothingQueued();
$fake->assertNothingStored();
$fake->assertNothingPruned();

Exchange::rates()->assertStored('EUR', 'CZK');        // the rates-table asserts also live on the faked rates()
```

| Assert | Passes when | Twin |
| --- | --- | --- |
| `assertRateRequested($from, $to)` | rate() was asked for the pair — conversions included. | `assertNothingRequested()` |
| `assertRefreshed(?$source, ?$feed)` | rates()->refresh() ran, optionally for this source and feed. | `assertNothingRefreshed()` |
| `assertRefreshQueued(?$source, ?$feed)` | rates()->refreshLater() ran. | `assertNothingQueued()` |
| `assertStored($from, $to, ?$rate, ?$source)` | store() or manual() got a rate for the pair; the rate is compared exactly. | `assertNothingStored()` |
| `assertPruned(?$before)` | rates()->prune() ran, optionally with this cut-off day. | `assertNothingPruned()` |

Currencies has no fake: the registry is in-memory and deterministic.

## Seeding and faking sources

```php
use Illuminate\Support\Facades\Http;
use RoundlyConsulting\Money\Models\CurrencyRate;

// Seed the rates table for the database driver (run the published migration in your test setup)
CurrencyRate::factory()->pair('EUR', 'USD', '1.10')->on('2026-09-25')->from('ecb')->create();

// Fake the ECB feed for the ecb driver
Http::fake(['*eurofxref-daily.xml' => Http::response($xml)]);
```

## Tips

- Build amounts with ofMinor() or ofMajor() and assert with equals() or minor() — both exact.
- For byte-exact formatting assertions use the decimal formatter; with intl, normalize whitespace — ICU versions swap NBSP and NNBSP.
- Freeze time with CarbonImmutable::setTestNow() for exchange tests — an undated lookup takes “today” in money.exchange.timezone.

```xml
<!-- phpunit.xml — byte-exact formatting on every machine -->
<php>
    <env name="MONEY_FORMATTER" value="decimal"/>
</php>
```

## 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/money-for-laravel.md)
