I have been reading about tests and I have found some texts saying I should have more unit tests than feature tests; something called the Test Pyramid.
I've started a simple system using TDD and so far I've basically written feature tests (example bellow), because I understood they mattered the most, since they are more related to what the user really does, such as making a post request (clicking a button that does it) and receiving a response. But because of what I read, I am now in doubt to wether I should keep doing this or if I should create unit tests that are going to test the service methods I call in my controllers.
And even if wrote unit test for each of my service methods, I'd still have more, or equal, feature tests than unit tests, because each method of my services is related to one endpoint and the user can send different data to it.
Also, If I am understanding test wrong, please, feel free to correct me. Thanks.
Tests:
<?php
namespace Tests\Feature\Cms;
use App\Models\Group;
use App\Models\Modules;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class UsersTest extends TestCase
{
use WithFaker, RefreshDatabase;
/** @test */
public function unauthenticated_users_are_redirected()
{
$this->withoutExceptionHandling();
$response = $this->get(route('cms.users.index'));
$response->assertRedirect();
}
/** @test */
public function a_user_can_be_created()
{
$this->withoutExceptionHandling()->signIn();
$password = $this->faker->password(6, 12);
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
'password' => $password,
'password_confirmation' => $password,
];
$response = $this->post(route('cms.users.store'), $user_data);
$response->assertSessionHas('response', cms_response(trans('cms.users.success_create')));
}
/** @test */
public function if_passwords_dont_match_when_creating_a_user_an_error_is_returned()
{
$this->signIn();
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
'password' => $this->faker->password(6, 12),
'password_confirmation' => $this->faker->password(6, 12),
];
$response = $this->post(route('cms.users.store'), $user_data);
$this->checkIfSessionErrorMatchesString('password', 'A senha e confirmação de senha não são iguais.');
$this->checkIfSessionErrorMatchesString('password_confirmation', 'A senha e confirmação de senha não são iguais.');
}
/** @test */
public function a_user_can_be_updated()
{
$this->withoutExceptionHandling()->signIn();
$user = User::factory()->withPassword($this->faker->password(6, 12))->create();
$password = $this->faker->password(6, 12);
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
'password' => $password,
'password_confirmation' => $password,
];
$response = $this->patch(route('cms.users.update', ['user' => $user->id]), $user_data);
$response->assertSessionHas('response', cms_response(trans('cms.users.success_update')));
}
/** @test */
public function if_the_user_isnt_found_an_error_is_returned()
{
$this->withoutExceptionHandling()->signIn();
$user = User::all()->last();
$password = $this->faker->password(6, 12);
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
'password' => $password,
'password_confirmation' => $password,
];
$response = $this->patch(route('cms.users.update', ['user' => $user->id + 1]), $user_data);
$response->assertSessionHas('response', cms_response(trans('cms.users.error_user_not_found'), false, 400));
}
/** @test */
public function a_user_can_be_updated_without_updating_its_password()
{
$this->withoutExceptionHandling()->signIn();
$user = User::factory()->withPassword($this->faker->password(6, 12))->create();
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
];
$response = $this->patch(route('cms.users.update', ['user' => $user->id]), $user_data);
$response->assertSessionHas('response', cms_response(trans('cms.users.success_update')));
}
/** @test */
public function if_passwords_dont_match_when_updating_a_user_an_error_is_returned()
{
$this->signIn();
$user = User::factory()->withPassword($this->faker->password(6, 12))->create();
$user_data = [
'email' => $this->faker->safeEmail(),
'fk_group_id' => Group::factory()->has(Modules::factory(), 'modules')->create()->id,
'name' => $this->faker->name(),
'password' => $this->faker->password(6, 12),
'password_confirmation' => $this->faker->password(6, 12),
];
$this->patch(route('cms.users.update', ['user' => $user->id]), $user_data);
$this->checkIfSessionErrorMatchesString('password', 'A senha e confirmação de senha não são iguais.');
$this->checkIfSessionErrorMatchesString('password_confirmation', 'A senha e confirmação de senha não são iguais.');
}
/** @test */
public function users_can_be_excluded()
{
$this->withoutExceptionHandling()->signIn();
$users_id = User::factory(2)->withPassword($this->faker->password(6, 12))->create()->pluck('id');
$response = $this->delete(route('cms.users.destroy', $users_id));
$response->assertSessionHas('response', cms_response(trans('cms.users.success_delete')));
}
}
Controller:
<?php
namespace App\Http\Controllers\Cms;
use App\Http\Controllers\Controller;
use App\Http\Requests\UserRequest;
use App\Services\CmsUsersService;
use Illuminate\Http\Request;
class UsersController extends Controller
{
private $service;
public function __construct(CmsUsersService $service)
{
$this->service = $service;
}
public function store(UserRequest $request)
{
$result = $this->service->create($request->all());
return redirect()->back()->with('response', $result);
}
public function update(UserRequest $request, $id)
{
$result = $this->service->update($id, $request->all());
return redirect()->back()->with('response', $result);
}
public function destroy($users_id)
{
$result = $this->service->delete($users_id);
return redirect()->back()->with('response', $result);
}
}
Service:
<?php
namespace App\Services;
use App\Models\User;
use Exception;
use Illuminate\Support\Facades\Hash;
use App\Interfaces\CRUD;
class CmsUsersService implements CRUD
{
public function create(array $data)
{
$data['token'] = Hash::make($data['email']);
$data['password'] = Hash::make($data['password']);
User::create($data);
return cms_response(trans('cms.users.success_create'));
}
public function update(int $id, array $data)
{
try {
if (array_key_exists('password', $data)) {
$data['password'] = Hash::make($data['password']);
}
$user = $this->__findOrFail($id);
$user->update($data);
return cms_response(trans('cms.users.success_update'));
} catch (\Throwable $th) {
return cms_response($th->getMessage(), false, 400);
}
}
public function delete(string $ids)
{
User::whereIn('id', json_decode($ids))->delete();
return cms_response(trans('cms.users.success_delete'));
}
private function __findOrFail(int $id)
{
$user = User::find($id);
if ($user instanceof User) {
return $user;
}
throw new Exception(trans('cms.users.error_user_not_found'));
}
}