codenco-dev/eloquent-model-tester
Laravel dev-only helper to test Eloquent models: verify table structure/columns, fillable vs guarded attributes, and model relationships. Works with PHPUnit and model factories, integrates easily in your model test classes.
Installation:
composer require codenco-dev/eloquent-model-tester --dev
Generate Test File:
php artisan make:test Models/UserTest
Basic Setup:
use App\Models\User;
use CodencoDev\EloquentModelTester\HasModelTester;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserTest extends TestCase
{
use RefreshDatabase, HasModelTester;
public function test_model_structure()
{
$this->modelTestable(User::class)
->assertHasColumns(['id', 'name', 'email']);
}
}
Verify a model's database schema and fillable/guarded attributes:
public function test_model_structure()
{
$this->modelTestable(User::class)
->assertHasColumns(['id', 'name', 'email', 'created_at', 'updated_at'])
->assertHasOnlyColumnsInFillable(['name', 'email'])
->assertHasTimestampsColumns();
}
// Basic column check
$this->modelTestable(User::class)
->assertHasColumns(['id', 'name', 'email']);
// Strict column check (no extra columns allowed)
->assertHasOnlyColumns(['id', 'name', 'email', 'created_at', 'updated_at']);
// Soft deletes
->assertHasSoftDeleteTimestampColumns();
// Check if columns are in fillable
->assertHasColumnsInFillable(['name', 'email']);
// Strict fillable check (only these should be fillable)
->assertHasOnlyColumnsInFillable(['name', 'email']);
// Check for guarded fields
->assertHasColumnsInGuarded(['password']);
// Strict guarded check
->assertHasOnlyColumnsInGuarded(['password']);
// Ensure no overlap between fillable and guarded
->assertNoGuardedAndFillableFields();
// One-to-One
->assertHasHasOneRelation(Phone::class);
// One-to-Many
->assertHasHasManyRelation(Order::class);
// Many-to-Many
->assertHasManyToManyRelation(Role::class);
// Custom keys for relations
->assertHasBelongsToRelation(Category::class, 'category', 'category_id');
// Morph relations
->assertHasHasManyMorphRelation(Comment::class, 'comments');
$this->modelTestable(Customer::class)
->assertHasBelongsToRelation(Category::class)
->assertHasBelongsToRelation(OtherModel::class)
->assertHasHasManyRelation(Order::class);
$this->tableTestable('role_user')
->assertHasColumns(['user_id', 'role_id', 'created_at']);
->assertHasScope('active')
->assertHasScope('popular');
Add RefreshDatabase to tests/TestCase.php to avoid repetition:
use Illuminate\Foundation\Testing\RefreshDatabase;
class TestCase extends BaseTestCase
{
use RefreshDatabase;
// ...
}
Organize tests in tests/Feature/Models/:
tests/
├── Feature/
│ ├── Models/
│ │ ├── UserTest.php
│ │ ├── PostTest.php
│ │ └── ...
Use factories to ensure data integrity before schema tests:
public function test_model_with_factory()
{
User::factory()->create(['name' => 'Test User']);
$this->modelTestable(User::class)
->assertHasColumns(['id', 'name', 'email']);
}
Create a base test class for shared assertions:
abstract class ModelTestCase extends TestCase
{
use HasModelTester;
protected function assertModelStructure($model, array $columns, array $fillable = [])
{
$this->modelTestable($model)
->assertHasOnlyColumns($columns)
->assertHasOnlyColumnsInFillable($fillable);
}
}
Add tests to your CI pipeline (e.g., GitHub Actions) to catch schema drift early:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: composer install
- run: php artisan test --filter="*Test"
assertHasOnlyColumnscreated_at/updated_at in assertHasOnlyColumns causes false failures.->assertHasOnlyColumns(['id', 'name', 'email', 'created_at', 'updated_at'])
ID vs id).->assertHasColumns(array_map('strtolower', ['ID', 'Name']))
assertHasSoftDeleteTimestampColumns fails if SoftDeletes trait isn’t used.use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use SoftDeletes;
}
user_id vs author_id) break assertions.->assertHasBelongsToRelation(Author::class, 'author', 'author_id')
$this->tableTestable('role_user')
->assertHasColumns(['user_id', 'role_id', 'created_at']);
active vs isActive) cause false negatives.->assertHasScope('isActive'); // Not 'active'
Use Laravel’s schema builder to debug column names:
public function test_schema_debug()
{
$columns = Schema::getColumnListing('users');
dd($columns); // Inspect actual column names
}
Extend the trait to log failures:
trait DebugModelTester
{
public function modelTestable($model)
{
$tester = parent::modelTestable($model);
$tester->setHandler(function ($message, $context) {
\Log::error($message, $context);
});
return $tester;
}
}
Test subsets of columns/relations incrementally:
// Test fillable first
$this->modelTestable(User::class)
->assertHasColumnsInFillable(['name']);
// Then add more
->assertHasColumnsInFillable(['email']);
Ensure RefreshDatabase is enabled for tests that modify the schema:
use RefreshDatabase; // Add to test class
php artisan migrate:fresh --env=testing
Extend the tester for project-specific rules:
use CodencoDev\EloquentModelTester\Testable;
class CustomTester extends Testable
{
public function assertHasCustomColumn($column)
{
return $this->seeInDatabase($this->model->getTable(), $column, function ($query) {
return $query->limit(1);
});
}
}
Generate relation tests dynamically:
public function test_all_relations()
{
$relations = (new User)->getRelations();
foreach ($relations as $relation) {
$this->modelTestable(User::class)
->assertHasRelation($relation);
}
}
Combine with laravel/pint for schema + code style checks:
composer require laravel/pint --dev
./vendor/bin/pint
php artisan test --filter="*Test"
How can I help you explore Laravel packages today?