VeldoraDocs

19. Testing & Model Factories

Veldora includes an expressive testing framework built on top of PHPUnit. Write feature tests that simulate real HTTP requests, assert on responses, and verify database state — without needing a real browser.

Setting Up Tests

Tests live in the tests/ directory. Run the full test suite with:

Terminal • bash
php vendor/bin/phpunit

Or run a single test file:

Terminal • bash
php vendor/bin/phpunit tests/Feature/PostTest.php

Writing HTTP Feature Tests

app/Models/Post.php
PHP
namespace Tests\Feature;

use Veldora\Framework\Testing\TestCase;
use Database\Factories\UserFactory;
use Database\Factories\PostFactory;

class PostTest extends TestCase
{
    public function test_guests_cannot_create_posts(): void
    {
        $response = $this->post('/posts', ['title' => 'Test', 'body' => 'Body']);
        $response->assertStatus(302);
        $response->assertRedirect('/login');
    }

    public function test_authenticated_user_can_create_post(): void
    {
        $user = (new UserFactory())->create();

        $response = $this->actingAs($user)->post('/posts', [
            'title' => 'My First Post',
            'body'  => 'This is the body content.',
        ]);

        $response->assertRedirect('/posts');
        $this->assertDatabaseHas('posts', ['title' => 'My First Post']);
    }

    public function test_user_can_view_their_posts(): void
    {
        $user = (new UserFactory())->create();
        $post = (new PostFactory())->create(['user_id' => $user->id, 'title' => 'My Post']);

        $response = $this->actingAs($user)->get('/posts');

        $response->assertOk();
        $response->assertSee('My Post');
    }

    public function test_user_cannot_delete_another_users_post(): void
    {
        $owner  = (new UserFactory())->create();
        $other  = (new UserFactory())->create();
        $post   = (new PostFactory())->create(['user_id' => $owner->id]);

        $response = $this->actingAs($other)->delete('/posts/' . $post->id);

        $response->assertStatus(403);
        $this->assertDatabaseHas('posts', ['id' => $post->id]); // Still exists
    }
}

All Response Assertion Methods

example.php
PHP
// HTTP status
$response->assertOk();              // status 200
$response->assertStatus(201);       // specific status code
$response->assertRedirect('/login'); // 302 redirect to URL
$response->assertNotFound();        // 404
$response->assertForbidden();       // 403

// Response body
$response->assertSee('Welcome');         // Contains string
$response->assertDontSee('Error');       // Does NOT contain string
$response->assertSeeText('Dashboard');   // Contains plain text

// JSON responses
$response->assertJson(['status' => 'ok']);          // JSON has these keys/values
$response->assertJsonCount(5, 'data');              // JSON array has N items
$response->assertJsonPath('data.0.title', 'Post 1'); // Specific JSON path value

Database Assertions

example.php
PHP
// Assert a record exists in the database
$this->assertDatabaseHas('posts', [
    'title'   => 'My Post',
    'user_id' => 42,
]);

// Assert a record does NOT exist in the database
$this->assertDatabaseMissing('posts', ['title' => 'Deleted Post']);

// Assert a record was soft-deleted (deleted_at is not null)
$this->assertSoftDeleted('posts', ['id' => 1]);

Model Factories

Factories let you generate fake model instances for tests and seeders without writing manual SQL or repetitive User::create(...) calls.

Generate a factory:

Terminal • bash
php veldora make:factory PostFactory

Define the factory:

app/Models/Post.php
PHP
// database/factories/PostFactory.php
namespace Database\Factories;

use App\Models\Post;
use Veldora\Framework\Database\Factory;

class PostFactory extends Factory
{
    protected string $model = Post::class;

    public function definition(): array
    {
        return [
            'user_id'      => (new UserFactory())->create()->id,
            'title'        => $this->faker->sentence(6),
            'slug'         => $this->faker->slug(),
            'body'         => $this->faker->paragraphs(3, true),
            'is_published' => 1,
            'published_at' => $this->faker->dateTimeBetween('-1 year', 'now')->format('Y-m-d H:i:s'),
        ];
    }
}

Use factories in tests:

example.php
PHP
// Create a single instance and persist to database
$post = (new PostFactory())->create();

// Create with overridden attributes
$draftPost = (new PostFactory())->create(['is_published' => 0]);

// Create multiple instances
$posts = (new PostFactory())->count(5)->create();

// Make instance WITHOUT persisting (for unit tests)
$post = (new PostFactory())->make();

Testing JSON API Endpoints

app/Models/Post.php
PHP
class PostApiTest extends TestCase
{
    public function test_api_returns_paginated_posts(): void
    {
        (new PostFactory())->count(20)->create();

        $response = $this->get('/api/posts?page=1');

        $response->assertOk();
        $response->assertJson(['meta' => ['per_page' => 15]]);
        $response->assertJsonCount(15, 'data');
    }

    public function test_api_requires_auth_for_store(): void
    {
        $response = $this->post('/api/posts', [
            'title' => 'Test',
            'body'  => 'Body',
        ]);

        $response->assertStatus(401);
    }

    public function test_api_creates_post_for_authenticated_user(): void
    {
        $user = (new UserFactory())->create();

        $response = $this->actingAs($user)->post('/api/posts', [
            'title' => 'New API Post',
            'body'  => 'This is the post body content here.',
        ]);

        $response->assertStatus(200);
        $response->assertJsonPath('title', 'New API Post');
        $this->assertDatabaseHas('posts', ['title' => 'New API Post', 'user_id' => $user->id]);
    }
}

Running the Test Suite

Terminal • bash
# Run all tests
php vendor/bin/phpunit

# Run a single test file
php vendor/bin/phpunit tests/Feature/PostTest.php

# Run tests matching a filter
php vendor/bin/phpunit --filter test_guests_cannot_create_posts

# Run with verbose output
php vendor/bin/phpunit --testdox


Building with AI Assistants?
Copy the Veldora AI Developer Prompt to teach ChatGPT, Claude, Cursor, or Gemini all framework APIs for error-free coding.