Chirp Beyond (Bootcamp) part 1 The missing tests

silver343

Silver343

Posted on September 12, 2023

Chirp Beyond (Bootcamp) part 1 The missing tests

Welcome to Chirp Beyond (Bootcamp). This series of guides is just some of the ways you can extend and adapt Chirp the app created in the Laravel bootcamp.

These guides will follow on from the Inertia.js path in the Laravel bootcamp.

Tests

To test the code you created in the Laravel Bootcamp we will use PHPUnit a testing framework for PHP which Laravel provided built-in support, additionally, both Laravel and inertia.js provide testing helpers.

Running Tests

Use the command php artisan test in your terminal to run the existing test suite.

A total of 24 tests should run all passing.

You will notice that any users and chirps you created whilst developing Chirp are now gone, this is because when testing we are using the same database as the local version of the app. We can solve this by updating the phpunit.xml file.


  <env name="APP_ENV" value="testing"/>
  <env name="BCRYPT_ROUNDS" value="4"/>
  <env name="CACHE_DRIVER" value="array"/>
- <!-- <env name="DB_CONNECTION" value="sqlite"/> -->
- <!-- <env name="DB_DATABASE" value=":memory:"/> -->
+ <env name="DB_CONNECTION" value="sqlite"/>
+ <env name="DB_DATABASE" value="./database/test.sqlite"/>
+ <env name="DB_FOREIGN_KEYS" value="true"/>
  <env name="MAIL_MAILER" value="array"/>
  <env name="QUEUE_CONNECTION" value="sync"/>
  <env name="SESSION_DRIVER" value="array"/>
Enter fullscreen mode Exit fullscreen mode

Factories

Before creating any new tests, we need to create model factories. Factories allow our application to create model resources using predefined data. This data is often fake data supplied via a package called fakerphp/faker.

To create a Chirp factory run the command php artisan make:factory ChirpFactory

The newly created Chirp factory can be found inside the database/factories directory.

Inside the factory, we need to update the definition() method.

public function definition(): array
{
    return [
-     //
+     'message' => fake()->paragraph(),
+     'user_id' => User::Factory()
    ];
}
Enter fullscreen mode Exit fullscreen mode

As you can see factories can be used inside other factories, here we are using the user factory included by default.

Update Base Test Case

The tests we will be creating will extend the base test case found inside the tests directory.

We will update the test case to use LazilyRefreshDatabase, this will allow our database to be refreshed between tests if needed.

 <?php

 namespace Tests;

 use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
+use Illuminate\Foundation\Testing\LazilyRefreshDatabase;

abstract class TestCase extends BaseTestCase
{

-    use CreatesApplication;
+    use CreatesApplication, LazilyRefreshDatabase;

}
Enter fullscreen mode Exit fullscreen mode

Test Structure.

I tend to structure my test files in a similar way to my application files. This not only helps me determine where tests should reside, but can also make it easier to find tests in the future.

During the creation of Chirper, we created several files that require testing. These files are:

  • Events/ChirpCreated.php
  • Http/Controllers/ChirpController.php
  • Listeners/SendChirpNotifications.php
  • Policies/ChirpPolicy.php

Create Tests

We will begin by testing ChirpPolicy.php.

To create a test class, we can use the command php artisan make:test Policies/ChirpPolicyTest, the newly created test can be found in tests/Feature/Policies.

We need to test four aspects of the ChirpPolicy

  • The update method returns true if the user is the chirps author.

  • The update method returns false if the user is not the chirps author.

  • The delete method returns true if the user is the chirps author.

  • The delete method returns false if the user is not the chirps author.

<?php

namespace Tests\Feature\Policies;

use App\Models\Chirp;
use App\Models\User;
use Tests\TestCase;

class ChirpPolicyTest extends TestCase
{
    public function test_policy_returns_true_if_chirp_to_be_updated_belongs_to_user()
    {
        $chirp = Chirp::factory()->create();

        $this->assertTrue($chirp->user->can('update', $chirp));
    }

    public function test_policy_returns_false_if_chirp_to_be_updated_does_not_belongs_to_user()
    {
        $user = User::factory()->create();

        $chirp = Chirp::factory()->create();

        $this->assertFalse($user->can('update', $chirp));
    }

    public function test_policy_returns_true_if_chirp_to_be_deleted_belongs_to_user()
    {
        $chirp = Chirp::factory()->create();

        $this->assertTrue($chirp->user->can('delete', $chirp));
    }

    public function test_policy_returns_false_if_chirp_to_be_deleted_does_not_belongs_to_user()
    {
        $user = User::factory()->create();

        $chirp = Chirp::factory()->create();

        $this->assertFalse($user->can('update', $chirp));
    }
}
Enter fullscreen mode Exit fullscreen mode

Chirp Controller Test

The next set of tests we will create will be for the Chirp Controller. To begin, we will create the test file using the command php artisan make:test Controllers/ChirpControllerTest

The test file will be created in tests/Feature/Controllers.

The ChirpController has four methods we need to test, not only do we need to test the expected (happy) path but also ensure the app handles errors and unexpected events correctly, therefore the majority of the tests are testing the validation rules used.

<?php

namespace Tests\Feature\Controllers;

use App\Models\Chirp;
use App\Models\User;
use Tests\TestCase;
use Inertia\Testing\AssertableInertia as Assert;

class ChirpControllerTest extends TestCase
{
    public function test_user_is_redirected_from_index_if_not_logged_in()
    {
        $response = $this->get(route('chirps.index'));

        $response->assertRedirect(route('login'));
    }

    public function test_index_returns_200()
    {
        $user = User::factory()->makeOne();

        $this->actingAs($user);

        $response = $this->get(route('chirps.index'));

        $response->assertOk();
    }

    public function test_index_has_chirps()
    {
        $user = User::factory()->create();

        $chirp = Chirp::factory(1)->create();

        $this->actingAs($user);

        $this->get(route('chirps.index'))
            ->assertInertia(fn (Assert $page) => $page
                ->component('Chirps/Index')
                ->has('chirps', 1, fn (Assert $page) => $page
                    ->where('message', $chirp->first()->message)
                    ->etc()
                    ->has('user', fn (Assert $page) => $page
                        ->where('id', $chirp->first()->user_id)
                        ->where('name', $chirp->first()->user->name)
                        ->missing('password')
                    )
                )
            );
    }

    public function test_new_chirp_can_be_created()
    {
        $this->assertDatabaseEmpty('chirps');

        $user = User::factory()->create();

        $this->actingAs($user);

        $response = $this->post(route('chirps.store'),['message' => 'test new message']);

        $this->assertDatabaseHas('chirps', [
            'message' => 'test new message',
        ]);

        $response->assertRedirect(route('chirps.index'));
    }

    public function test_new_chirp_must_have_message()
    {
        $user = User::factory()->create();

        $this->actingAs($user);

        $response = $this->post(route('chirps.store'),[]);

        $response->assertSessionHasErrors([
            'message' => 'The message field is required.'
        ]);
    }

    public function test_new_chirp_message_must_be_string()
    {
        $user = User::factory()->create();

        $this->actingAs($user);

        $response = $this->post(route('chirps.store'),['message' => 123]);

        $response->assertSessionHasErrors([
            'message' => 'The message field must be a string.'
        ]);
    }

    public function test_new_chirp_message_must_be_less_than_255_characters()
    {
        $user = User::factory()->create();

        $this->actingAs($user);

        $response = $this->post(route('chirps.store'),['message' => str_repeat('a',256)]);

        $response->assertSessionHasErrors([
            'message' => 'The message field must not be greater than 255 characters.'
        ]);
    }

    public function test_chirp_can_be_updated()
    {
        $chirp = Chirp::factory()->create();

        $this->actingAs($chirp->user);

        $response = $this->patch(route('chirps.update', $chirp),['message' => 'test new message']);

        $this->assertDatabaseHas('chirps', [
            'message' => 'test new message',
        ]);

        $response->assertRedirect(route('chirps.index'));
    }

    public function test_updated_chirp_must_have_message()
    {
        $chirp = Chirp::factory()->create();

        $this->actingAs($chirp->user);

        $response = $this->patch(route('chirps.update',$chirp->id),[]);

        $response->assertSessionHasErrors([
            'message' => 'The message field is required.'
        ]);
    }

    public function test_updated_chirp_message_must_be_string()
    {
        $chirp = Chirp::factory()->create();

        $this->actingAs($chirp->user);

        $response = $this->patch(route('chirps.update',$chirp->id),['message' => 123]);

        $response->assertSessionHasErrors([
            'message' => 'The message field must be a string.'
        ]);
    }

    public function test_updated_chirp_message_must_be_less_than_255_characters()
    {
        $chirp = Chirp::factory()->create();

        $this->actingAs($chirp->user);

        $response = $this->patch(route('chirps.update',$chirp->id),['message' => str_repeat('a',256)]);

        $response->assertSessionHasErrors([
            'message' => 'The message field must not be greater than 255 characters.'
        ]);
    }

    public function test_chirp_cannot_be_updated_by_non_owner()
    {
        $chirp = Chirp::factory()->create();

        $wrongUser = User::factory()->create();

        $this->actingAs($wrongUser);

        $response = $this->patch(route('chirps.update', $chirp),['message' => 'test new message']);

        $response->assertForbidden();
    }

    public function test_chirp_can_be_deleted()
    {
        $chirp = Chirp::factory()->create();

        $this->assertDatabaseCount('Chirps',1);

        $this->actingAs($chirp->user);

        $response = $this->delete(route('chirps.destroy',$chirp));

        $response->assertRedirect(route('chirps.index'));

        $this->assertDatabaseEmpty('chirps');
    }

    public function test_chirp_cannot_be_delete_by_non_owner()
    {
        $chirp = Chirp::factory()->create();

        $this->assertDatabaseCount('Chirps',1);

        $wrongUser = User::factory()->create();

        $this->actingAs($wrongUser);

        $response = $this->delete(route('chirps.destroy',$chirp));

        $response->assertForbidden();

        $this->assertDatabaseCount('Chirps',1);
    }
}
Enter fullscreen mode Exit fullscreen mode

The tests cover a range of functionality, including checking that users are redirected if not logged in, that chirps can be created, updated, and deleted, and that validation rules are enforced for chirp messages. The tests also check that only the owner of a chirp can update or delete it.

To learn more about testing with Inertia.js, you can visit the official documentation.

Chirp Created Event Test

To create the tests for the ChirpCreated event use the command php artisan:make Events/ChirpCreatedTest

This will create our test file tests/Feature/Events/ChirpCreatedTest.php which we will update like so.

<?php

namespace Tests\Feature\Events;

use App\Events\ChirpCreated;
use App\Models\User;
use Illuminate\Support\Facades\Event;
use Tests\TestCase;

class ChirpCreatedtest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_chirp_created_event_is_dispatched()
    {
        Event::fake();

        $user = User::factory()->create();

        $this->actingAs($user);

        $this->post(route('chirps.store'),['message' => 'test new message']);

        Event::assertDispatched(ChirpCreated::class);
    }
}
Enter fullscreen mode Exit fullscreen mode

With this single test we are ensuring that the event is dispatched when a chirp is created, you will see we are using Event::fake();

This allows us to prevent any listeners from executing and assert against our events, you can find more about this at the Laravel docs.

Send Chirp Created Notifications Test

Create the test class for our listener using the command php artisan make:test Listeners/SendChirpCreatedNotificationsTest

We will update the new test class at tests/feature/Listeners.

namespace Tests\Feature\Listeners;

use App\Events\ChirpCreated;
use App\Listeners\SendChirpCreatedNotifications;
use App\Models\Chirp;
use App\Models\User;
use App\Notifications\NewChirp;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;

class SendChirpCreatedNotificationsTest extends TestCase
{
    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function test_listining_for_chirp_created_event()
    {
        Event::fake();

        Event::assertListening(
            ChirpCreated::class,
            SendChirpCreatedNotifications::class
        );
    }

    public function test_notification_is_sent_to_all_but_chirp_creator()
    {
        Notification::fake();

        $users = User::factory(2)->create();

        $chirp = Chirp::factory()->create();

        Notification::assertSentTo(
            [$users], NewChirp::class
        );

        Notification::assertNotSentTo(
            [$chirp->user], NewChirp::class
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

We are again using Event::fake() to create assertions, this time we are asserting that our listener is attached to the ChirpCreated Event.

In the second test we are using Notification::fake(), this allows us to assert if and to who the notifications were sent. read more

We have now created all the tests for the Chirper application. These tests cover the Chirp Policy, Chirp Controller, and ChirpCreated event. We also tested the listener responsible for sending notifications when a new chirp is created. With these tests, we can be confident that our application is working as expected.

💖 💪 🙅 🚩
silver343
Silver343

Posted on September 12, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related