해리의 데브로그

Test Driven Laravel 09 - Book Checkout 테스트 코드 구현 (Feature test)

|

Coder’s tape의 Test Driven Laravel 강의 를 듣고 정리한 포스팅 입니다.

1. Checkout 테스트 코드 작성

Unit Test에서 구현한 book checkout, checkin을 Feature Test에서 구현

php artisan make:test BookCheckoutTest

Unit Test의 로직을 기본적으로 따라감

// Unit Test
public function a_book_can_be_checked_out()
{
  $book = factory(Book::class)->create();
  $user = factory(User::class)->create();

  $book->checkout($user);

  $this->assertCount(1, Reservation::all());
  $this->assertEquals($user->id, Reservation::first()->user_id);
  $this->assertEquals($book->id, Reservation::first()->book_id);
  $this->assertEquals(now(), Reservation::first()->checked_out_at);
}

Feature Test의 경우 라우트의 엔드포인트를 통해 인증된 유저가 들어올 것이라고 가정을 하고 진행하므로 접근방법이 조금 다름. book_id를 함께 엔드포인트로 넘김. 만약 제대로 동작할 경우, 나머지 코드는 Unit Test의 코드를 동일하게 사용

  • actingAs 헬퍼 메소드는 특정사용자를 현재 사용자로 인증하는 단순한 방법을 제공함.
class BookCheckoutTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function a_book_can_be_checked_out_by_a_signed_in_user()
    {
        $book = factory(Book::class)->create();
        $user = factory(User::class)->create();

        $this->actingAs($user)->post('/checkout/' . $book->id);

        $this->assertCount(1, Reservation::all());
        $this->assertEquals($user->id, Reservation::first()->user_id);
        $this->assertEquals($book->id, Reservation::first()->book_id);
        $this->assertEquals(now(), Reservation::first()->checked_out_at);
    }
}

에러가 발생. 상세하게 어떠한 에러인지를 알기 위해 withoutExceptionHandling 사용

Failed asserting that actual size 0 matches expected size 1.

라우트 설정

// Exception\NotFoundHttpException : POST http://localhost/checkout/1
Route::post('/checkout/{book}', 'CheckoutBookController@store');

CheckoutBookController 컨트롤러 생성 후 store 메소드 구현

// BindingResolutionException : Target class [App\Http\Controllers\CheckoutBookController] does not exist.
class CheckoutBookController extends Controller
{
    public function store()
    {
    }
}

Unit Test를 통해서 바로 다음 어떠한 로직/코드를 짜야하는지 쉽게 알 수 있음. Route Model Binding을 통해 Book 객체를 저장시키며, auth()->user() 를 통해 인증된 유저를 인자로 받아 Book@checkout 를 실행시킴.

class CheckoutBookController extends Controller
{
    public function store(Book $book)
    {
        $book->checkout(auth()->user());
    }
}

나머지 코드는 Unit Test 때와 그대로 동일하게 작성

public function a_book_can_be_checked_out_by_a_signed_in_user()
{
  $book = factory(Book::class)->create();
  $user = factory(User::class)->create();

  $this->actingAs($user)->post('/checkout/' . $book->id);

  $this->assertCount(1, Reservation::all());
  $this->assertEquals($user->id, Reservation::first()->user_id);
  $this->assertEquals($book->id, Reservation::first()->book_id);
  $this->assertEquals(now(), Reservation::first()->checked_out_at);
}

2. Checkout 테스트 케이스 확장 - 인증된 유저만 체크아웃 가능

인증되지 않은 유저가 체크아웃을 진행할 경우, 로그인 페이지로 리다이렉트하는 테스트 케이스 작성. Reservation은 이루어지지 않았으므로 객체의 수는 0개여야 함.

/** @test */
public function only_signed_in_users_can_checkout_a_book()
{
  $book = factory(Book::class)->create();

  $this->post('/checkout/' . $book->id)
    ->assertRedirect('/login');

  $this->assertCount(0, Reservation::all());
}

테스트를 돌려보면 500 에러가 발생함. withoutExceptionHandling 를 통해 출력을 해보면 user_id가 존재하지 않음을 알수 있음. (Book@checkout에서 reservation 객체를 생성할 때 user_id가 필요하나, 누락되어있음)

// Response status code [500] is not a redirect status code.

// Book.php
public function checkout($user)
{
  $this->reservations()->create([
    'user_id' => $user->id,
    'checked_out_at' => now(),
  ]);
}

생성자 메소드를 통해 라라벨의 미들웨어중 하나인 Authenticate 를 동작시킴. Authenticate 파일을 살펴보면 인증에 실패했을 경우, 로그인 페이지로 리다이렉트 시키는 로직이 구현되어있음.

// Http\Middleware\Authenticate.php
class Authenticate extends Middleware
{
    protected function redirectTo($request)
    {
        if (! $request->expectsJson()) {
            return route('login');
        }
    }
}
class CheckoutBookController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }
    public function store(Book $book)
    {
        $book->checkout(auth()->user());
    }
}

테스트를 동작시키면 500에러가 발생함. 현재 Laravel authentication을 이용하고 있지만, 그에 따른 라우트 설정을 해주지 않았음(로그인, 로그아웃 등의 라우트가 존재하지 않음).

//  if  laravel version below 6.0
php artisan make:auth 
// else
composer require laravel/ui --dev 
php artisan ui vue --auth

이후 테스트를 돌려보면 성공적으로 작동하는 것을 알 수 있음.

3. Checkout 테스트 케이스 확장 - 책이 존재할 때만 체크아웃 가능

존재하지 않는 book_id를 하드코딩하여 입력한 후, assertStatus 가 404(not found)가 뜨도록 코드 작성

/** @test */
public function only_real_books_can_be_checked_out()
{
  $this->actingAs(factory(User::class)->create())
    ->post('/checkout/123')
    ->assertStatus(404);

  $this->assertCount(0, Reservation::all());
}

Comments