PostgreSQL Permissions for Laravel Tests: Fix Errors
Fix PostgreSQL permission errors in Laravel tests: isolate a test database, grant schema access, configure PHPUnit, and use RefreshDatabase safely. Start here.

Fix PostgreSQL Permissions Before Running Laravel Tests
A Laravel feature test can fail before it ever touches your application code. One common culprit is PostgreSQL blocking Laravel's attempt to create its migrations table, with an error like permission denied for schema public.
In other words: the database connection may work, and the role may even be able to access the database, but that still doesn't mean it can create tables inside the public schema.
The reliable fix is to treat test setup like real infrastructure: use a dedicated test database, grant the configured role the permissions it needs there, and make sure PHPUnit and Laravel are pointed at the same database. Then use Laravel's RefreshDatabase trait when you need a predictable database state. This guide walks through those steps, clarifies the difference between database-level and schema-level permissions, and shows how to tell a genuine PostgreSQL failure from unrelated PHP deprecation warnings. It also covers what a passing setup test does - and doesn't - prove about your API.
Why Laravel gets a PostgreSQL permission error
Say you generate a Laravel feature test and run it. Before the test can make an HTTP request or assert on a response, Laravel first prepares the database. That often includes creating (or updating) the migration history table.
Then PostgreSQL responds with something like:
SQLSTATE[42501]: Insufficient privilege: 7
ERROR: permission denied for schema public
SQL: create table "migrations" (...)
This usually isn't "a bad migration" or "a broken test." It's PostgreSQL enforcing a permission boundary. PostgreSQL permissions aren't one-size-fits-all: a role can be able to connect to a database and still be missing the permissions required to create objects in a specific schema.
Database access and schema access are different
Here's the split that matters for this problem:
- Database privileges cover things like connecting to a database.
- Schema privileges cover actions inside a namespace in the database - like creating objects in that schema.
- Table privileges cover actions on specific tables, like reading and updating rows.
Creating a table is a schema-level operation. In a typical Laravel setup, migrations create tables in the public schema unless you've configured otherwise. So being allowed to connect to samu_test doesn't automatically mean the role can create a table in public.
Ownership can be the reason too. In the example that prompted this fix, the application role could access the test database, but the database and public schema were owned by the PostgreSQL administrator role. The application role couldn't create the migration table - so the test failed during setup, before it ever got to the assertions.
Why the test database should be separate
Tests that run migrations can create, alter, or roll back schema objects. Some cleanup flows can also remove data. Running that against a development database creates unnecessary risk: one misconfiguration can damage local data or make your tests depend on whatever happens to exist on that machine.
A dedicated database gives you a clean boundary. It also makes permissions easier to reason about: you can grant the test role exactly what it needs in the test database, without handing out broad access to production or other environments. The exact commands vary across PostgreSQL versions and hosting setups, but the principle stays the same: isolate test data and grant only what the test role needs.
Point PHPUnit and Laravel at the test database
Fixing permissions won't help if the test runner connects to the wrong database. Make sure Laravel's test configuration matches your dedicated PostgreSQL setup: host, database name, user, and connection settings all need to line up.
Laravel projects typically define test settings in phpunit.xml, a .env.testing file, or both. Don't assume every project loads settings in the same order - check your Laravel version and current configuration.
For example, phpunit.xml might include:
<env name="APP_ENV" value="testing"/>
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_PORT" value="5432"/>
<env name="DB_DATABASE" value="samu_test"/>
<env name="DB_USERNAME" value="samu_user"/>
If PostgreSQL requires password authentication, provide DB_PASSWORD via a local or CI secret. Don't commit a production password to phpunit.xml.
| Setting | What it controls |
|---|---|
APP_ENV |
Selects Laravel's testing environment |
DB_CONNECTION |
Chooses the database driver, here pgsql |
DB_HOST |
Identifies the PostgreSQL server |
DB_PORT |
Identifies the PostgreSQL port, commonly 5432 |
DB_DATABASE |
Selects the dedicated test database |
DB_USERNAME |
Selects the PostgreSQL role used by the test process |
DB_PASSWORD |
Supplies authentication when the server requires it |
Check for configuration conflicts
If .env.testing exists, compare its database settings with what PHPUnit is configuring. A mismatch makes debugging painful: you can grant permissions for samu_test and still have tests connect somewhere else.
Also check whether config was cached. In local development, clearing Laravel's config cache helps after you change environment settings:
php artisan config:clear
In CI, verify the environment variables passed into the test job too. Values injected by the runner or container can differ from what you expect based on local files. Don't guess which connection is active - confirm what the test process is actually using.
Laravel's database testing documentation and configuration documentation are good references when behavior differs across versions or project setups.
Use RefreshDatabase and verify the result
Once the connection and permissions are correct, RefreshDatabase can set up the database state for tests. Add it to a feature test when the test depends on database state:
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class MenuCategoryApiTest extends TestCase
{
use RefreshDatabase;
public function test_menu_categories_table_exists(): void
{
$this->assertDatabaseHas('migrations', [
'migration' => '2026_09_18_034452_create_menu_categories_table',
]);
}
}
RefreshDatabase prepares the test database using Laravel's migration and transaction mechanisms as appropriate for your app and test environment. It helps tests start from a known baseline, but it's not a substitute for correct PostgreSQL permissions and correct connection settings. Laravel still has to connect to the database you configured and run the migrations it needs.
Make the assertion match the question
That example assertion checks that a specific migration appears in Laravel's migrations table. It assumes the migration ran and that the filename matches the string in the test. This can be useful as a setup check, but it doesn't prove that your menu-category API behaves correctly.
If you're only trying to verify that a table exists, a schema-level assertion can express that intent more directly. If what you actually care about is application behavior, test behavior: make a request, check the response, and verify the resulting database state. A test that only confirms a migration record can still pass even if your endpoint has broken validation, authorization, or response logic.
After fixing configuration, run the focused test:
php artisan test --filter=MenuCategoryApiTest
If it passes, that's good evidence that Laravel could prepare the configured database and that the assertions succeeded. It's still not proof that the full suite or every API operation works - so once the focused test is green, run the whole suite:
php artisan test
Treat deprecation warnings as a separate issue
It's totally possible for tests to pass while PHP prints deprecation warnings. For example, a warning about implicitly nullable parameters in a package like nunomaduro/collision doesn't mean PostgreSQL failed to create tables.
Investigate package compatibility on its own by checking versions:
php -v
composer show nunomaduro/collision
composer show laravel/framework
composer show phpunit/phpunit
Before changing dependencies, verify which versions are compatible with your installed PHP, Laravel, and PHPUnit. Updating one package without checking the rest can swap one failure mode for another. Keep the diagnosis clear: a PostgreSQL permission error is a database access problem; a PHP deprecation warning is a compatibility/maintenance issue.
Extend tests beyond database setup
For a menu-category API, the next steps should test the behavior users actually rely on:
| Test area | What to verify |
|---|---|
| List categories | The endpoint returns the expected records and response shape |
| Create category | Valid input creates a record and returns the expected response |
| Validate input | Invalid or missing fields produce the intended validation response |
| Show category | The requested record is returned, and missing records are handled correctly |
| Update category | Changes are persisted and reflected in the response |
| Delete category | The record is removed or otherwise handled according to the API contract |
| Prevent duplicates | The application enforces its intended uniqueness rules |
| Validate relationships | Warehouse or kitchen-section references are checked and persisted correctly |
You can test migration details separately (columns, foreign keys, constraints). Just keep the layers distinct: database setup tests confirm the environment is usable; feature tests confirm the application behaves correctly.
Conclusion
A PostgreSQL permission error during Laravel test setup usually means your test role can reach the database but can't create the objects Laravel needs in its target schema. The durable fix is to avoid "broad privileges everywhere" and instead isolate a dedicated test database, choose between ownership and explicit grants for that environment, and verify permissions at both the database and schema levels.
Then make PHPUnit and Laravel point to the exact same database/role. Clear cached config when needed, and use RefreshDatabase when your tests depend on database state. Start with a focused test to confirm setup works, then run the full suite and expand to real API behavior. If you're debugging this in your project, check the active connection and the public schema permissions first - those details often explain the failure before you touch application code.