skip to content

In a Laravel test, how do assertDatabaseHas, assertDatabaseCount and assertModelExists check that a warehouse stock movement was saved?

level: juniorimportance: should knowfreq 55%

answer

  1. one row must match every column
  2. table name, model class or model
  3. assertModelExists checks by primary key
  4. assertDatabaseMissing and assertModelMissing
  5. castAsJson for JSON columns

basics

~20 s

assertDatabaseHas('stock_movements', [...]) passes when at least one row matches every given column; assertDatabaseCount checks the table's row count; assertModelExists($movement) checks the model's primary key is still in its table. Each has a Missing or Empty counterpart.

solid answer

~40 s

`assertDatabaseHas($table, $data)` runs a `where($data)->exists()` query: one row must match **all** the given columns at once. The first argument can be a table name, a model class such as `StockMovement::class` (its table and connection are used), or a model instance, in which case its primary key is added to the conditions. `assertDatabaseMissing` is the inverse, `assertDatabaseCount('stock_movements', 3)` checks a row count and `assertDatabaseEmpty` checks none. `assertModelExists($movement)` is shorthand for `assertDatabaseHas($movement)`, checking by key, and `assertModelMissing` confirms a delete. The checks read raw column values, so compare what is stored: use `castAsJson()` for JSON columns rather than a PHP array, and remember casts such as encryption are not applied for you.

code

php · 28 lines
php
<?php

namespace Tests\Feature;

use App\Models\Product;
use App\Models\StockMovement;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class ReceiveStockAssertionsTest extends TestCase
{
    use RefreshDatabase;

    public function test_receipt_is_stored_once(): void
    {
        $product = Product::factory()->create(['on_hand' => 0]);

        $this->post("/products/{$product->id}/receive", ['quantity' => 50]);

        $this->assertDatabaseHas(StockMovement::class, [
            'product_id' => $product->id,
            'quantity' => 50,
            'meta' => $this->castAsJson(['source' => 'dock-2']),
        ]);
        $this->assertDatabaseCount('stock_movements', 1);
        $this->assertDatabaseHas($product, ['on_hand' => 50]);
    }
}

go deeper

for a junior

Use assertDatabaseHas with a table and the columns you care about, and assertDatabaseCount when the number of rows matters.

for a middle

Explain that one row must match all columns, that a model class or instance can replace the table name, and why JSON columns need castAsJson.

for a senior

Read a surprising count as an isolation signal, pick model-based assertions after deletes, and keep query-count expectations for hot paths only.

for a principal

Decide how much database-level assertion the suite needs versus response assertions, so tests pin behaviour without freezing the schema.

## What the assertions are for After a feature test posts to the warehouse app's "receive stock" endpoint, the HTTP response only tells half the story. Database assertions check the other half: did the right rows land in the right tables? Laravel provides them on the base test case, through the `InteractsWithDatabase` concern. | Assertion | Passes when | |---|---| | `assertDatabaseHas($table, $data)` | at least one row matches all of `$data` | | `assertDatabaseMissing($table, $data)` | no row matches all of `$data` | | `assertDatabaseCount($table, $n)` | the table has exactly `$n` rows | | `assertDatabaseEmpty($table)` | the table has no rows | | `assertModelExists($model)` | the model's primary key is present in its table | | `assertModelMissing($model)` | the model's primary key is absent | | `expectsDatabaseQueryCount($n)` | the test runs exactly `$n` queries | ## How assertDatabaseHas matches Under the hood the assertion builds `DB::connection(...)->table($table)->where($data)->exists()`. Three consequences follow: 1. **All columns in one row.** `['product_id' => 7, 'quantity' => 50]` passes only if a single row has both values. Two rows, one with each value, do not satisfy it. 2. **Extra columns are ignored.** The row may have any other values; list only what the test cares about. 3. **Raw column values.** Eloquent casts do not run. A JSON column must be compared with `$this->castAsJson([...])`, which builds the database's JSON cast expression; an encrypted cast stores ciphertext, so assert on something else or decrypt through the model. The first argument is flexible: - a **table name**, `'stock_movements'`; - a **model class**, `StockMovement::class`, which resolves the table name and the model's connection; - a **model instance**, which adds its primary key to the conditions and uses its connection; - an **iterable of models**, checking each. A third argument names a connection explicitly when the table lives elsewhere. ## assertModelExists and assertModelMissing `assertModelExists($movement)` is literally `assertDatabaseHas($movement)`: it checks that a row with the model's key exists in its table. It reads well after a factory call or a create endpoint that returns the model, and `assertModelMissing($movement)` is the natural check after a delete endpoint. Soft deletes behave differently, because the row stays; soft-delete assertions are covered with soft-delete behaviour itself. ## Counting and emptiness `assertDatabaseCount('stock_movements', 1)` is the strongest check that an endpoint created exactly one row and not two. It is also sensitive to leftovers: if an earlier test committed rows that no reset trait removed, the count is off, so a failing count is often the first sign that database isolation is broken. `assertDatabaseEmpty()` suits "a rejected request must not write anything". ## Choosing assertions - Prefer `assertDatabaseHas` with the handful of columns that matter, not every column. - Add `assertDatabaseCount` when duplicates would be a bug, such as a double-submitted receipt. - Use `assertModelExists` / `assertModelMissing` when you already hold the model. - Use `expectsDatabaseQueryCount` sparingly, to pin down a query count that must not creep up; it fails on any change, including harmless ones. ## Database assertions or model reads? A test can also check persistence by reading through Eloquent, for example `$product->fresh()->on_hand`. The two styles answer different questions: - **Database assertions** check the stored row directly, independent of the model's casts, accessors and global scopes. They catch a mismatch between what the model shows and what the table holds. - **Model reads** check what the rest of the application will see, casts and scopes included. Most endpoint tests want the stored truth, so `assertDatabaseHas` is the usual first choice; reach for model reads when the behaviour under test is itself about casts or scopes. Either way, pass the connection name as the third argument when the table lives on a connection other than the default, or the assertion looks in the wrong database and reports a confusing miss. ## A warehouse example Receiving 50 units of `BOLT-M8` should create one movement and update the product's stock: - `assertDatabaseHas('stock_movements', ['product_id' => $product->id, 'quantity' => 50, 'type' => 'receipt'])` - `assertDatabaseCount('stock_movements', 1)` - `assertDatabaseHas($product, ['on_hand' => 50])`, where the model instance adds its key. A junior candidate should reach for `assertDatabaseHas` unprompted; interviewers then ask what "has" means when several columns are given, and why a JSON column comparison fails.

  • Why does assertDatabaseHas('stock_movements', ['meta' => ['source' => 'dock-2']]) fail even though the row is there?
    The assertion compares raw column values with a `where` query, and an array is not how the JSON column is stored or compared. Wrap the value in `$this->castAsJson(['source' => 'dock-2'])`, which builds the database's JSON cast expression, so the comparison happens between JSON values.
  • When would assertDatabaseCount catch a bug that assertDatabaseHas misses?
    When an endpoint writes a row twice, for example on a double-submitted receipt. `assertDatabaseHas` passes as soon as one matching row exists; `assertDatabaseCount('stock_movements', 1)` fails on the duplicate. The same count also exposes rows leaked by a test without a reset trait.

saying these in an interview costs you the question

  • assertDatabaseHas passes if each column matches in some row, not necessarily the same one.
  • assertDatabaseHas applies the model's casts before comparing values.
  • assertModelExists compares every attribute of the model with the row.
  • assertDatabaseHas fails when the row has columns the test did not list.
  • assertDatabaseHas only accepts a literal table name.