DEV Community

Jonathan Ruiz
Jonathan Ruiz

Posted on

Laravel 11 Simple API Rest with tests

First let’s create the database using phpMyAdmin
database: simple_api_rest
collation: utf8mb4_unicode_ci

Create a laravel project using one of this alternatives.

composer create-project laravel/laravel simple_api_rest
Enter fullscreen mode Exit fullscreen mode
composer global require laravel/installer

laravel new simple_api_rest
Enter fullscreen mode Exit fullscreen mode

Don’t select a starter kit.
We will use PHPUnit for testing.
Select mysql as a database.

Now run the command

php artisan install:api
Enter fullscreen mode Exit fullscreen mode

This command create a api.php file in routes folter.
Each route defined in that file have the /api prefix by default.
Remove the route that was generated.

Creating the model Post with their migration.

php artisan make:model Post -m
Enter fullscreen mode Exit fullscreen mode

This command will create the migration in database/migration
And the model in app/Models

public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->text('content');
            $table->timestamps();
        });
    }
Enter fullscreen mode Exit fullscreen mode

Creating the PostFactory.

php artisan make:factory PostFactory
Enter fullscreen mode Exit fullscreen mode

This command will create the factory in database/factories

public function definition(): array
    {
        return [
            "title" => $this->faker->title(),
            "content" => $this->faker->paragraph(),
        ];
    }
Enter fullscreen mode Exit fullscreen mode

Creating the PostSeeder

php artisan make:seeder PostSeeder
Enter fullscreen mode Exit fullscreen mode

This command will create the seeder in database/seeders

use App\Models\Post;

class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory(10)->create();
    }
}
Enter fullscreen mode Exit fullscreen mode

Open the file seeders/DatabaseSeeder

public function run(): void
    {
        $this->call([
            PostSeeder::class,
        ]);
    }
Enter fullscreen mode Exit fullscreen mode

Run the migration and the seeder

php artisan migrate:fresh  # fresh will remove all the data saved
php artisan db:seed # run the seeders
or
php artisan migrate:fresh --seed
Enter fullscreen mode Exit fullscreen mode

You can see data created in the posts table in phpMyAdmin
Creating a test to view if data was created

php artisan make:test PostTest
Enter fullscreen mode Exit fullscreen mode

This command will create the test in tests/Feature

use PHPUnit\Framework\Attributes\Test;

class PostTest extends TestCase
{
    #[Test]
    public function posts_must_be_created(): void
    {
        $this->assertDatabaseCount('posts', 10);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the tests

php artisan test # run all the tests
or
php artisan test --filter posts_must_be_created # run all test that matches
Enter fullscreen mode Exit fullscreen mode

Using a database in memory to run the tests
Open phpunit.xml and uncomment two lines

<php>
    <env name="APP_ENV" value="testing"/>
    <env name="APP_MAINTENANCE_DRIVER" value="file"/>
    <env name="BCRYPT_ROUNDS" value="4"/>
    <env name="CACHE_STORE" value="array"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="MAIL_MAILER" value="array"/>
    <env name="PULSE_ENABLED" value="false"/>
    <env name="QUEUE_CONNECTION" value="sync"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="TELESCOPE_ENABLED" value="false"/>
</php>
Enter fullscreen mode Exit fullscreen mode

If we run the tests again will throw an error because the seeder wasn’t executed.

class PostTest extends TestCase
{
    use RefreshDatabase; // Create the database and run the migrations in each test

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(PostSeeder::class); // Run the seeder PostSeeder
    }
    #[Test]
    public function posts_must_be_created(): void
    {
        $this->assertDatabaseCount('posts', 10);
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating controller

php artisan make:controller PostController --api --model=Post
Enter fullscreen mode Exit fullscreen mode

This command will create the controller in app/Http/Controllers

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::all();
        return response()->json($posts);
    }
    public function store(Request $request)
    {
        $data = $request->validate([
            'title' => 'required|string|max:255',
            'content' => 'required|string',
        ]);
        $post = Post::create($data);
        return response()->json($post, 201);
    }
    public function show(Post $post)
    {
        return response()->json($post);
    }
    public function update(Request $request, Post $post)
    {
        $data = $request->validate([
            'title' => 'sometimes|required|string|max:255',
            'content' => 'sometimes|required|string',
        ]);
        $post->update($data);
        return response()->json($post);
    }
    public function destroy(Post $post)
    {
        $post->delete();
        return response()->json(null, 204);
    }
}
Enter fullscreen mode Exit fullscreen mode

Defining routes

use App\Http\Controllers\PostController;
use Illuminate\Support\Facades\Route;

Route::apiResource('posts', PostController::class);
Enter fullscreen mode Exit fullscreen mode

Command to see the routes defined

php artisan route:list
or
php artisan r:l
Enter fullscreen mode Exit fullscreen mode

CRUD tests

class PostTest extends TestCase
{
    use RefreshDatabase; // Create the database and run the migrations in each test

    protected function setUp(): void
    {
        parent::setUp();
        $this->seed(PostSeeder::class); // Run the seeder PostSeeder
    }
    #[Test]
    public function posts_must_be_created(): void
    {
        $this->assertDatabaseCount('posts', 10);
    }

    #[Test]
    public function must_show_post_list(): void
    {
        $response = $this->getJson(route('posts.index'));
        $response->assertStatus(200);
        $response->assertJsonCount(10);
        $response->assertJsonStructure([
            '*' => [
                'id',
                'title',
                'content',
                'created_at',
                'updated_at',
            ]
        ]);
    }

    #[Test]
    public function must_show_post(): void
    {
        $response = $this->getJson(route('posts.show', 1));
        $response->assertStatus(200);
        $response->assertJsonStructure([
            'id',
            'title',
            'content',
            'created_at',
            'updated_at',
        ]);
    }
    #[Test]
    public function must_create_post(): void
    {
        $response = $this->postJson(route('posts.store'), [
            'title' => 'Test Post',
            'content' => 'This is a test post',
        ]);
        $response->assertStatus(201);
        $response->assertJsonStructure([
            'id',
            'title',
            'content',
            'created_at',
            'updated_at',
        ]);
        $this->assertDatabaseHas('posts', [
            'title' => 'Test Post',
            'content' => 'This is a test post',
        ]);
    }

    #[Test]
    public function must_update_post(): void
    {
        $response = $this->putJson(route('posts.update', 1), [
            'title' => 'Updated Test Post',
            'content' => 'This is an updated test post',
        ]);
        $response->assertStatus(200);
        $response->assertJsonStructure([
            'id',
            'title',
            'content',
            'created_at',
            'updated_at',
        ]);
        $this->assertDatabaseHas('posts', [
            'id' => 1,
            'title' => 'Updated Test Post',
            'content' => 'This is an updated test post',
        ]);
    }

    #[Test]
    public function must_delete_post(): void
    {
        $response = $this->deleteJson(route('posts.destroy', 1));
        $response->assertStatus(204);
        $this->assertDatabaseMissing('posts', [
            'id' => 1,
        ]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Running tests
Update and create tests will throw an error
We’re using mass assignment but still not defined the attributes that can received.

Open Post in app/Models

class Post extends Model
{
    use HasFactory;

    protected $fillable = [
        'title',
        'content',
    ];
}
Enter fullscreen mode Exit fullscreen mode

Runing the tests again

php artisan test
or
php artisan test --filter PostTest
Enter fullscreen mode Exit fullscreen mode

Top comments (0)