In a Laravel test, how do assertDatabaseHas, assertDatabaseCount and assertModelExists check that a warehouse stock movement was saved?
answer
- one row must match every column
- table name, model class or model
- assertModelExists checks by primary key
- assertDatabaseMissing and assertModelMissing
- castAsJson for JSON columns
basics
~20 sassertDatabaseHas('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
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
Use assertDatabaseHas with a table and the columns you care about, and assertDatabaseCount when the number of rows matters.
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.
Read a surprising count as an isolation signal, pick model-based assertions after deletes, and keep query-count expectations for hot paths only.
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.