Part 2 - Implementing testing and organizing the code

In part 1, we implemented the authentication module. Now we will ensure we follow the best practices. So let's start with testing.

Feature Testing

Authentication is a feature of our app. The user can register, login and log out. These are actions a user can do on the app. Any actions a user can take is called feature whereas independent functions in code are called units. Let's begin with coding test cases for features.

There are 3 actions a user can perform:

  1. Register

  2. Login

  3. Logout

Let's create a feature test

php artisan make:test AuthenticationTest

It will create a file tests\Feature\AuthenticationTest.php

<?php

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Tests\TestCase;

class AuthenticationTest extends TestCase {
    use RefreshDatabase;

    public function test_user_can_register(): void {
        $data = [
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => 'password123',
        ];

        $response = $this->post('/api/register', $data);

        $response->assertStatus(200)
            ->assertJsonStructure([
                'user' => ['id', 'name', 'email', 'created_at', 'updated_at'],
                'token',
            ]);
    }

    public function test_user_can_login(): void {
        User::create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => Hash::make('password123'),
        ]);

        $data = [
            'email' => 'john@example.com',
            'password' => 'password123',
        ];

        $response = $this->post('/api/login', $data);

        $response->assertStatus(200)
            ->assertJsonStructure([
                'user' => ['id', 'name', 'email', 'created_at', 'updated_at'],
                'token',
            ]);
    }

    public function test_user_can_logout(): void {
        $user = User::create([
            'name' => 'John Doe',
            'email' => 'john@example.com',
            'password' => Hash::make('password123'),
        ]);

        $token = $user->createToken('test-token')->plainTextToken;

        $response = $this->withHeaders(['Authorization' => "Bearer $token"])
            ->json('POST', '/api/logout');

        $response->assertStatus(200);
        $this->assertEmpty($user->fresh()->tokens);
    }
}

We can run the tests now

php artisan test

What about unit tests?

Unit tests are used to test smaller units of our code. To make our code more testable we distribute the code in units where each unit is doing one action which can be independently invoked and tested. In other words, we need to design an architecture for our code so our logic is in the form of small units.

In the previous part, we wrote a Controller. What is the purpose of a controller? A controller has to receive the request and return a response. Before returning a response, there could be some actions that need to be performed. Consider this example

public function store(CreatePostRequest $request): JsonResponse {
    $data = $request->all();

    // using transaction because we have multiple queries

    DB::beginTransaction();

    try {

    } catch (Exception $e) {

    }
    // create a post
    $post = Post::create($data['post'])

    // add the photos
    $post->photos()->attach($data['photos']);

    return response()->json([
        'post' => $post,
    ], 201);
}

If we look at this, this is an action with multiple queries and implementing this in the controller makes it messy. So we will create services that are composed of functions or units that do some task and return or produce a result. This increases the testability of the code as every action now would be completely isolated.

Second, where should we write the queries? The models contain relations, mutators, accessors and things related to a single instance. Putting queries in the model would be confusing. So just to make sure queries related to one model are in one place and could be more reusable we will implement the repository pattern.

Now we set some rules

  1. One model is only used in one service. If another service requires something from that model, it should not directly call the model. Instead, it will call the service that operates on the model.

  2. One model is referenced in one repository only.

  3. Services will use Repositories.

  4. Repositories will use Models.

  5. A single method of a repository will have a single query. For actions requiring multiple queries, we will have a service function that calls multiple repository methods in a transaction.

Now let's refactor the code.

Implementing the Repository Pattern

Create folder App\Repositories. Inside this folder, we can create another folder named Contracts. This folder will contain the interfaces for the repositories. This is an optional thing to do. It is a part of the repository pattern to make sure the repositories implement the methods in the same way each time. It was done this way to make sure if the database engine or library is swapped, the new repositories are implemented according to the interface design. But now we have ORMs that make swapping the database not a problem. Inside the Eloquent this assurance is already provided. But in case in the future I have to use a database that is not supported by Eloquent or its data access layer does not have similar query language I would have to reimplement everything. But this is highly unlikely. If someone has built a data access package for Laravel, it would have the same principles as Eloquent. So it is entirely optional. I am going with the conventional way with contracts.

Create App\Repositories\Contracts\UserRepositoryInterface.php

namespace App\Repositories\Contracts;

use App\Models\User;

interface UserRepositoryInterface {
    function findByEmail ($email): User;
}

Create App\Repositories\BaseRepository.php. There would be some methods reused in each repository. Let's implement them in a base repository.

namespace App\Repositories;

use Illuminate\Database\Eloquent\Model;

class BaseRepository {
    protected Model $model;

    public function create (array $attributes) {
        return $this->model->create($attributes);
    }
}

Create App\Repositories\UserRepository.php

namespace App\Repositories;

use App\Models\User;
use App\Repositories\Contracts\UserRepositoryInterface;

class UserRepository extends BaseRepository implements UserRepositoryInterface  {
    public function __construct (User $model) {
        $this->model = $model;
    }

    public function findByEmail ($email): User {
        return $this->model->where('email', $email)->first();
    }
}

Creating Services

We will create a UsersService to manage actions related to users. There will be an AuthenticationService that handles the Authentication by using the UsersService. Authentication is not part of UsersService logically, so we are putting the authentication part in the AuthenticationService and the user part in the UsersService. We want relevancy based on the logical responsibilities of the services. It will make testable actions in each service. You can see the UsersService contains methods that could be reused for some other actions in the future.

App\Services\UsersService.php

namespace App\Services;

use App\Models\User;
use App\Repositories\UserRepository;

class UsersService {
    public function __construct(private UserRepository $userRepository) {}

    public function createUser (array $attributes): User {
        return $this->userRepository->create($attributes);
    }

    public function getUserByEmail ($email): User {
        return $this->userRepository->findByEmail($email);
    }

    public function deleteCurrentToken (User $user): void {
        $user->currentAccessToken()->delete();
    }
}

App\Services\AuthenticationService.php

namespace App\Services;

use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Auth;

class AuthenticationService {
    public function __construct(private UsersService $usersService) {}

    public function register (array $attributes): User {
        return $this->usersService->createUser($attributes);
    }

    public function authenticate ($credentials): User {
        if (Auth::attempt($credentials)) {
            return $this->usersService->getUserByEmail($credentials);
        } else {
            throw new Exception("Invalid Credentials", 401);
        }
    }

    public function logout (User $user): void {
        $this->usersService->deleteCurrentToken($user);
    }
}

Refactored AuthenticationController

namespace App\Http\Controllers;

use App\Http\Requests\LoginUserRequest;
use App\Http\Requests\RegisterUserRequest;
use App\Services\AuthenticationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class AuthenticationController extends Controller {
    public function __construct(private AuthenticationService $authenticationService) {}

    public function register (RegisterUserRequest $request): JsonResponse {
        $user = $this->authenticationService->register($request->all());

        $token = $user->createToken('token');

        return response()->json([
            'user' => $user,
            'token' => $token->plainTextToken
        ]);
    }

    public function login (LoginUserRequest $request): JsonResponse {
        $user = $this->authenticationService->authenticate($request->all());

        $token = $user->createToken('token');

        return response()->json([
            'user' => $user,
            'token' => $token->plainTextToken
        ]);
    }

    public function logout (Request $request): JsonResponse {
        $this->authenticationService->logout($request->user());

        return response()->json([]);
    }
}

The code now is elegant and readable. Now after changing so much code, you have to test everything again. Hmm, right? But since we wrote test cases, it is as simple as running php artisan test. Everything tested again, without creating new users and all the hassle!

Next up we will work on responses. Just reflect on the above code. It could be made more beautiful.