해리의 데브로그

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

|

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

1. GET STARTED

도서관에서 책을 대여하는 checkout 구현하기 위해, 테스트 파일 생성

php artisan make:test BookReservationsTest

가장 기본적으로, 책을 체크아웃하면 시간이 찍히는 로직 구현. 기준이 되는 모델을 유저로 해도 되며, 책으로 해도 됨. 후자를 선택해서 진행

public function a_book_can_be_checked_out()
{
  // $book->checkout($user);
  // $user->checkout($book);
}

2. Factory

factory() 를 활용하여 Book, User 모델을 토대로 가상의 데이터를 생성하여 각각의 변수에 저장. Reservation 모델은 구현 전이나, user_id, book_id, check_out_at 등의 컬럼을 갖고 있을 예정.

use App\Book;
use App\User;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;

class BookReservationsTest extends TestCase
{
    use RefreshDatabase;
    /** @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/unit test 인지를 살펴보면, 우리가 체크아웃하는 특정한 책을 토대로 테스트를 하기 때문에 unit test라고 할 수 있음.

3. Feature Test vs Unit Test

Feature Test 중 하나인 an_author_can_be_created 를 살펴보면,

  1. The test actually hits a particular endpoint with some data.
  2. we are basically simulating the same exact thing that a user would do if they went through our browser, interacting with our website sent in some data.
  3. application did what it had to do

Unit Test인 a_book_can_be_checked_out 의 경우, “we are at no point doing anything that a user could do directly”. 따라서 해당 테스트의 파일 위치를 tests\Feature에서 tests\Unit으로 변경

/** @test */
public function an_author_can_be_created()
{
  $this->withoutExceptionHandling();

  $this->post('/author', [
    'name' => 'Author Name',
    'dob' => '05/14/1998'
  ]);

  $author = Author::all();

  $this->assertCount(1, $author);
  $this->assertInstanceOf(Carbon::class, $author->first()->dob);
  $this->assertEquals('1998/14/05', $author->first()->dob->format('Y/d/m'));
}

4. Factory 생성

테스트를 돌려보면 에러가 발생. 따라서 모델 Book에 대한 팩토리를 만듦.

InvalidArgumentException : Unable to locate factory with name [default] [App\Book].

php artisan make:factory BookFactory -m Book

마이그레이션에 명시된 컬럼명들을 토대로 코드를 작성. 이때 author_id에 대해서는 2가지 방법으로 구현이 가능함.

/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Book;
use App\Author;
use Faker\Generator as Faker;

$factory->define(Book::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence,
      	'author_id' => factory(Author::class)->create() // 방법 1
      	'author_id' => factory(Author::class) // 방법 2
    ];
});

후자의 방법의 경우,

  1. 테스트 코드에서 book을 생성할 때 값을 전달시킬 경우, 라라벨은 Author 객체를 생성하지 않음.
  2. 만약에 값을 전달시키지 않을 경우, 라라벨은 Author 객체를 생성시킴.

그러나 전자의 경우, book을 생성할 때마다 author 또한 생성 됨. 만약에 수동으로 author_id를 오버로딩하는 경우에도 author_id가 생성됨. 따라서 후자의 방법을 사용

/** @test */
public function a_book_can_be_checked_out()
{
  $book = factory(Book::class)->create([
    'author_id' => 123
  ]);

Author 팩토리가 생성되지 않았다는 에러가 발생하므로 팩토리 생성

InvalidArgumentException : Unable to locate factory with name [default] [App\Author].
/** @var \Illuminate\Database\Eloquent\Factory $factory */

use App\Author;
use Faker\Generator as Faker;

$factory->define(Author::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'dob' => now()->subYears(10),
    ];
});

3. Return to check out 테스트 코드 구현

Book 모델 내 checkout 메소드 구현

BadMethodCallException : Call to undefined method App\Book::checkout()
class Book extends Model
{
    // 코드 중략
    public function checkout()
    {
    }
}

Reservation 모델 생성 & 테스트 파일에 모델 import

Error : Class 'Tests\Unit\Reservation' not found
php artisan make:model Reservation -m

Book 모델에 checkout 메소드를 구현. a_book_can_be_checked_out 에서 Book@checkout 를 호출하여, Reservation 객체를 생성시킨 후, user_id, checked_out_at의 값을 적용시킴. 이때 book_id도 필요함 (관계 설정이 요구 됨)

Failed asserting that actual size 0 matches expected size 1.
// BookReservatinsTest.php
public function a_book_can_be_checked_out()
{
  $book = factory(Book::class)->create();
  $user = factory(User::class)->create();

  $book->checkout($user);

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

Reservation은 Book과 관계설정(Book: Reservation = 1: N) 이 적용되므로, 코드를 아래와 같이 수정

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

Book 모델에 관계설정 메소드 구현

BadMethodCallException : Call to undefined method App\Book::reservations() 
public function reservations()
{
  return $this->hasMany(Reservation::class);
}

Reservation 모델에 mass assignment 코드 적용

Illuminate\Database\Eloquent\MassAssignmentException : Add [user_id] to fillable property to allow mass assignment on [App\Reservation].
namespace App;

use Illuminate\Database\Eloquent\Model;

class Reservation extends Model
{
    protected $guarded = [];
}

Reservation 모델 마이그레이션에 누락된 컬럼들을 추가

Illuminate\Database\QueryException : SQLSTATE[HY000]: General error: 1 table reservations has no column named user_id
public function up()
{
  Schema::create('reservations', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->unsignedBigInteger('user_id');
    $table->unsignedBigInteger('book_id');
    $table->timestamp('checked_out_at');
    $table->timestamps();
  });
}

Test Driven Laravel 06 - Book:create with author & Unit test (2)

|

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

1. Returned to Feature Test

Feature test로 돌아와, a_new_author_is_automatically_added 테스트를 돌리면 또 다른 에러가 발생함.

table books has no column named author

이는 엔드포인트를 author로 hit하고 있기 때문임. 컨트롤러 수정

public function validateRequest()
{
  return request()->validate([
    'title'  => 'required',
    // author -> author_id로 변경
    'author_id' => 'required',
  ]);
}

Author 모델 내에서 작성한 메소드를 Book 모델으로 이동시킨 후, 메소드명을 수정(Author->AuthorId). 그리고 attirbutes의 key 또한 author -> author_id로 변경시킴. 이 메소드를 통해 string으로 들어온 $author을 Author 객체로 변환시킴.

public function setAuthorIdAttribute($author)
{
  $this->attributes['author_id'] = Author::firstOrCreate([
    'name' => $author,
  ]);
}

validation exception 에러 발생

The given data was invalid.

라우트에 넘기는 데이터를 ‘author’ 에서 ‘author_id’로 변경. 여기서 author_id 값에 왜 author의 name에 해당하는 string값을 넣어주는것에 대해 의문이 생길 수 있으나, 모델의 mutator 메소드 setAuthorIdAttribute 에서 이 값을 기반으로 author 객체를 만든 후, id값을 저장시키는 로직을 작성할 것임.

/** @test */
public function a_new_author_is_automatically_added()
{
  $this->withoutExceptionHandling();

  $this->post('/books', [
    'title' => 'cool book title',
    // author -> author_id 로 변경
    'author_id' => 'harry' 
  ]);

에러 발생

Failed asserting that '{"name":"harry","updated_at":"2019-12-16 05:37:37","created_at":"2019-12-16 05:37:37","id":1}' matches expected 1.
Expected :1
Actual   :{"name":"harry","updated_at":"2019-12-16 05:37:37","created_at":"2019-12-16 05:37:37","id":1}

이는 모델의 mutator 메소드에서 author_id 값으로 객체 전체의 정보를 저장시키고 있기 때문임. id값이 저장되도록 변경

public function setAuthorIdAttribute($author)
{
  $this->attributes['author_id'] = (Author::firstOrCreate([
    'name' => $author,
  ]))->id;
}

이제 성공적으로 테스트가 동작함

// BookManagementTest.php - test
/** @test */
public function a_new_author_is_automatically_added()
{
  $this->withoutExceptionHandling();

  $this->post('/books', [
    'title' => 'cool book title',
    'author_id' => 'harry'
  ]);

  $book = Book::first();
  $author = Author::first();

  $this->assertEquals($author->id, $book->author_id);
  $this->assertCount(1, Author::all());
}

// Book.php - Model
public function setAuthorIdAttribute($author)
{
  $this->attributes['author_id'] = (Author::firstOrCreate([
    'name' => $author,
  ]))->id;
}

// BooksController.php - controller
public function validateRequest()
{
  return request()->validate([
    'title'  => 'required',
    'author_id' => 'required',
  ]);
}

// create_books_table.php - migration
public function up()
{
  Schema::create('books', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->unsignedBigInteger('author_id');
    $table->timestamps();
  });
}

2. Test Code 수정

테스트를 전체로 돌려보면 여러 추가 에러가 발생함.

Failed asserting that actual size 0 matches expected size 1.
 Desktop/TDD/library/tests/Feature/BookManagementTest.php:26
 

Session missing error: author
Failed asserting that false is true.
 Desktop/TDD/library/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:1028
 Desktop/TDD/library/tests/Feature/BookManagementTest.php:50
 

Error : Call to a member function path() on null
 Desktop/TDD/library/tests/Feature/BookManagementTest.php:65
 

Failed asserting that actual size 0 matches expected size 1.
 Desktop/TDD/library/tests/Feature/BookManagementTest.php:84

3. Book - store 코드 수정

BookManagementTest@a_book_can_be_added_to_the_library (store 메소드)에서 에러가 발생

public function a_book_can_be_added_to_the_library()
{
  $response = $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  $book = Book::first();

  $this->assertCount(1, Book::all());
  $response->assertRedirect($book->path());
}

코드 수정에 앞서, 데이터를 넘기는 부분이 반복적으로 사용되고 있음. 이부분을 extract하여 새로운 메소드로 구현. 그리고 author이 아니라 author_id를 넘김.

// before
$response = $this->post('/books', [
  'title' => 'cool book title',
  'author' => 'harry'
]);

// after
private function data()
{
  return [
    'title'  => 'cool book title',
    'author_id' => 'harry'
  ];
}
/** @test */
public function a_book_can_be_added_to_the_library()
{
  $this->withoutExceptionhandling();

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

  $book = Book::first();

  $this->assertCount(1, book::all());
  $response->assertRedirect($book->path());
}

4. Book - author validation 코드 수정

기존 코드

public function an_author_is_required()
{
  $response = $this->post('/books', [
    'title' => 'cool book title',
    'author' => ''
  ]);

  $response->assertSessionHasErrors('author');
}

PHP에서 지원하는 array_merge 를 활용함(두 배열을 합쳐주는 기능을 함). assertSessionHasErrors 의 값도 author에서 author_id로 변경

/** @test */
public function an_author_is_required()
{
  $response = $this->post('/books', array_merge($this->data(), ['author_id' => '']));

  $response->assertSessionHasErrors('author_id');
}

5. Book - delete 코드 수정

엔드포인트를 때릴때 데이터를 넘기는 부분을 새롭게 구현한 메소드로 변경

public function a_book_can_be_deleted()
{
  // $this->post('/books', [
  // 'title' => 'cool book title',
  // 'author' => 'harry'
  // ]);
  
  $this->post('/books', $this->data());

  $book = Book::first();
  $this->assertCount(1, Book::all());

  $response = $this->delete('/books/' . $book->id);

  $this->assertCount(0, Book::all());
  $response->assertRedirect('/books');
}

6. Book - Update 코드 수정

엔드포인트를 때릴 때 넘기는 데이터는 새로운 메소드로 변경. 그리고 새로운 데이터로 업데이트할 때, author_id를 당연히 넘겨줘야한다. 마지막으로 assertEquals에는 author_id값을 비교하되, 1이 아니라 2를 비교함. (새로운 author을 만들었으므로 그 author의 id 값은 2가 될 것)

public function a_book_can_be_updated()
{
  $this->post('/books', $this->data());

  $book = Book::first();

  $response = $this->patch($book->path(), [
    'title' => 'New Title',
    'author_id' => 'victor',
  ]);

  $this->assertEquals('New Title', Book::first()->title);
  $this->assertEquals(2, Book::first()->author_id);
  $response->assertRedirect($book->fresh()->path());
}

Test Driven Laravel 05 - Book:create with author & Unit test (1)

|

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

1. GET STARTED

책(데이터)을 생성할 때, 작가를 자동적으로 생성하는 테스트 케이스를 작성해봄. 책 작성 시 작가를 자동적으로 생성하므로, AuthorManagementTest.php가 아닌 BookManagementTest.php에서 작성

/** @test */
public function a_new_author_is_automatically_added()
{
  $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  $book = Book::first();
  $author = Author::first();

  $this->assertEquals($author->id, $book->author_id);
  $this->assertCount(1, Author::all());
}

2. firstOrCreate

현재 코드로는 ‘harry’라는 string이 입력되므로, author 객체가 생성되지 않음. 따라서 author(string)값이 생성되면, 이 값을 기반으로 (존재한다면) 매칭되는 객체를 가지고 오거나, 새로운 객체를 생성하는 메소드를 모델에서 구현

  • 라라벨 헬퍼 메소드 firstOrCreate 활용
public function setAuthorAttribute($author)
{
  $this->attributes['author_id'] = Author::firstOrCreate([
    'name' => $author,
  ]);
}

하지만 여전히 에러가 발생함. 빈 객체의 id값을 갖고오는 이유는 author을 생성할 때 필요한 dob값을 지정해주지 않아 데이터베이스에 저장이 되지 않고 있기 때문임. 따라서 setAuthorAttribute($author) 메소드도 에러가 발생해야함.

ErrorException: trying to get property 'id' of non-object

에러 메세지를 확인하기 위해 withoutExceptionHandling 메소드를 찍어보아도 동일한 에러메세지가 뜸.

/** @test */
public function a_new_author_is_automatically_added()
{
  $this->withoutExceptionHandling();

3. Unit Test - only_name_is_required_to_create_an_author

현재 feature test를 진행하고 있음. Feature test를 라우트를 통해 진행되나, 어떤 에러가 발생하는지 디테일하게 확인을 못하고 있는 상황임 (when you are testing too high up in terms of layers, it occurs). 이 때 “drop down level”을 이용하며, unit test를 사용.

unit test는 lowest level로 클래스와 객체들과 직접적으로 상호작용이 가능함. Test\Unit 내 AuthorTest.php 생성 후 기본 코드 작성

namespace Tests\Unit;

use App\Author;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class AuthorTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function only_name_is_required_to_create_an_author()
    {
        Author::firstOrCreate([
            'name' => 'John Doe',
        ]);

        $this->assertCount(1, Author::all());
    }
}

이후, 테스트를 진행하면 드디어, NULL constraint violation 에러가 발생함. Feature에서 진행한 테스트의 결과로는 구체적인 에러 메세지를 확인할 수 없었음. (never be able to reach down lower level) . 따라서 unit에서 테스트를 진행한 것임.

date of birth 값을 추가하지 않았으므로, 데이터베이스 저장에 실패함. 따라서 이 필드가 null값이 가능하도록 마이그레이션 파일 수정

public function up()
{
  Schema::create('authors', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('name');
    // nullable
    $table->timestamp('dob')->nullable();
    $table->timestamps();
  });
}

위의 경우는 왜 발생했는지를 직관적으로 알 수 있으므로 개발을 진행하면서 직접 마이그레이션 내 코드를 수정하여 에러를 제거 시킬 수 있지만 이는 TDD의 step(아래) 대로 진행하지 않은 것임.

  1. we start with green test
  2. we change the test
  3. we make it fail
  4. we change the code on a fail test to turn it into green

4. Unit Test - an_author_id_is_recorded

이후 다시 feature test로 돌아오면 여전히 에러가 발생하는데, 다음 단계로 $bookauthor_id를 갖고 있지 않다는 것을 알고 있음.

/** @test */
public function a_new_author_is_automatically_added()
{
  $this->withoutExceptionHandling();

  $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  $book = Book::first();
  $author = Author::first();

  $this->assertEquals($author->id, $book->author_id);
  $this->assertCount(1, Author::all());
}

다시 drop down a level 한 후 에러를 수정시켜 봄.

php artisan make:test BookTest --unit

현재 우리는 모델을 통해 데이터를 at the very root level of our project에서 생성하고 있음. 엔드포인트를 통해서 테스트를 진행하는 것이 아님. Unit test라고 말 할 수 있음 (we are reaching in and really grabbing the stuff in the database and manipulating)

namespace Tests\Unit;

use App\Book;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookTest extends TestCase
{
  use RefreshDatabase;
  
    /** @test */
    public function an_author_id_is_recorded()
    {
        Book::create([
            'title' => 'cool title',
            'author_id' => 1,
        ]);

        $this->assertCount(1, Book::all());
    }
}

에러 발생

table books has no column named author_id 

book migration 수정 & author_id 컬럼 추가

public function up()
{
  Schema::create('books', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    $table->string('author');
    $table->unsignedBigInteger('author_id');
    $table->timestamps();
  });
}

Constraint Violation 에러 발생

NOT NULL constraint failed: books.author 

book migration 파일에서 더이상 사용하지 않는 author 칼럼 제거한 후 유닛 테스트를 동작시키면 제대로 동작함.

public function up()
{
  Schema::create('books', function (Blueprint $table) {
    $table->bigIncrements('id');
    $table->string('title');
    // $table->string('author');
    $table->unsignedBigInteger('author_id');
    $table->timestamps();
  });
}

Test Driven Laravel 04 - Author:create 테스트 코드 구현 / Carbon Instance

|

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

1. Author - Create 테스트 코드 작성

테스트 파일 생성

php artisan make:test AuthorManagementTest

author 생성하는 테스트 케이스 기본 코드 작성

class AuthorManagementTest extends TestCase
{
    use RefreshDatabase;
    /** @test */
    public function an_author_can_be_created()
    {
        $this->withoutExceptionHandling();

        $this->post('/author', [
            'name' => 'Author Name',
            'dob' => '05/14/1998'
        ]);

        $this->assertCount(1, Author::all());
    }
}

라우트 & 컨트롤러 추가

Route::post('/author', 'AuthorsController@store');

class AuthorsController extends Controller
{
    public function store()
    {
    }
}

모델 생성 & 마이그레이션 적용

php artisan make:model Author -m

모델에 mass assignment 설정

class Author extends Model
{
    protected $guarded = [];
}

마이그레이션 파일 수정

Schema::create('authors', function (Blueprint $table) {
  $table->bigIncrements('id');
  $table->string('name');  // 추가
  $table->timestamp('dob'); // 추가
  $table->timestamps();
});

모델을 테스트 파일과컨트롤러 파일에 import 시킨 후, 기본 코드 작성

use App\Author;

class AuthorsController extends Controller
{
    public function store()
    {
        Author::create(request()->only([
            'name', 'dob',
        ]));
    }
}

2. Carbon Instance

Carbon은 PHP에서 개발한 날짜 관리 클래스로, Carbon을 이용하면 날짜를 쉽게 다룰 수 있음. 따라서 dob(Data of Birth)가 Carbon Instance로 오는지를 확인

use Carbon\Carbon;

public function an_author_can_be_created()
{
  $this->withoutExceptionHandling();

  $this->post('/author', [
    'name' => 'Author Name',
    'dob' => '05/14/1998'
  ]);

  $author = Author::all();
  $this->assertCount(1, $author);
  $this->assertInstanceOf(Carbon::class, $author->first()->dob);
}

현재는 Carbon Instance가 아니라고 에러가 발생함.

Failed asserting that '05/14/1998' is an instance of class "Carbon\Carbon".

라라벨 엘로퀸트와 연동시켜, Model 클래스의 $dates 변수에 날짜/시간 컬럼을 명시하면 자동으로 Carbon 객에로 생성이 됨.

class Author extends Model
{
    protected $guarded = [];

    protected $dates = ['dob'];
}

또다른 에러 발생… 포맷팅을 커스텀으로 설정해줘야함.

InvalidArgumentException : Unexpected data found.
Unexpected data found.
Data missing

포맷팅을 맞추기 위해서는 모델 클래스에서 메소드를 구현하되, 컨벤션 룰을 정확히따라줘야함. 인수명을 $attribute이라고 하는 것이 통상적이나, 우리는 이 상황에서 $dob가 들어오는 것을 알고있으므로 간단히 $dob 라고 명명.

  • 컨벤션 : set + 컬럼명(첫글자 대문자로 시작) + Attribute
class Author extends Model
{
    protected $guarded = [];

    protected $dates = ['dob'];

    public function setDobAttribute($dob)
    {
        $this->attributes['dob'] = Carbon::parse($dob);
    }
}

다른 포맷으로 들어오더라도, 포맷팅만 잘 비교해주면 상관없음

$this->assertEquals('1998/14/05', $author->first()->dob->format('Y/d/m'));

Test Driven Laravel 03 - Book:Delete 테스트 코드 구현 / Redirect 리팩토링

|

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

1. Delete 테스트 코드 작성

데이터를 삭제하는 테스트 케이스도 유사한 방식으로 생성 가능

/** @test */
public function a_book_can_be_deleted()
{
  $this->withoutExceptionHandling();
  
  $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  $book = Book::first();
  $this->assertCount(1, Book::all());

  $response = $this->delete('/books/' . $book->id);

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

DELETE 메소드 라우터에 추가

Route::delete('/books/{book}', 'BooksController@destroy');

컨트롤러에 destroy 메소드 추가

public function destroy(Book $book)
{
  $book->delete();
}

2. Delete - Redirect

데이터가 삭제 된 이후, index페이지로 리다이렉트 설정

// BooksController@a_book_can_be_deleted
$response = $this->delete('/books/' . $book->id);

$this->assertCount(0, Book::all());
$response->assertRedirect('/books');
// Bookscontroller.php
public function destroy(Book $book)
{
  $book->delete();

  return redirect('/books');
}

3. Update - Redirect

데이터를 업데이트한 이후, show(상세페이지)로 리다이렉트 설정

/** @test */
public function a_book_can_be_updated()
{
  $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  $book = Book::first();

  $response = $this->patch('/books/' . $book->id, [
    'title' => 'New Title',
    'author' => 'victor',
  ]);

  $this->assertEquals('New Title', Book::first()->title);
  $this->assertEquals('victor', Book::first()->author);

  // redirect 추가
  $response->assertRedirect('/books/' . $book->id); 
}

컨트롤러내 update 메소드에서도 리다이렉트 추가

public function update(Book $book)
{
  $book->update($this->validateRequest());

  return redirect('/books/' . $book->id);
}

4. Create(Store) Redirect

동일한 방법으로 데이터 생성 후, show(상세페이지)로 리다이렉트 설정

public function a_book_can_be_added_to_the_library()
{
  $response = $this->post('/books', [
    'title' => 'cool book title',
    'author' => 'harry'
  ]);

  // 인스턴스 저장($book->id를 넘기기 위해 필요)
  $book = Book::first();  

  $response->assertOk();
  $this->assertCount(1, Book::all());

  // 리다이렉트 설정
  $response->assertRedirect('/books/' . $book->id);
}
public function store()
{
  $book = Book::create($this->validateRequest());

  return redirect('/books/' . $book->id);
}

테스트를 동작시키면, 302 에러 발생함. 따라서 200 응답을 반환하는 코드인 아래 코드를 삭제해줘야함.

//Response status code [302] does not match expected 200 status code.
$response->assertOk(); // 제거할 것

5. 리다이렉트 리팩토링 (1)

상세페이지(show)로 리다이렉트시키는 아래의 코드가 반복적으로 사용되고 있음. 이를 모델의 메소드로 구현하여 리팩토링을 할 수 있음.

  • Book@path 메소드 구현
return redirect('/books/' . $book->id);
class Book extends Model
{
    protected $guarded = [];

    public function path()
    {
        return '/books/' . $this->id;
    }
}
public function store()
{
  $book = Book::create($this->validateRequest());

  // return redirect('/books/' . $book->id);
  return redirect($book->path());
} 

BookReservationTest.php내에서 기존의 리다이렉트 포맷을 갖고 있는 코드를 업데이트 시킴

// before
$response->assertRedirect(('/books/' . $book->id));
// after
$response->assertRedirect($book->path());

6. 리다이렉트 리팩토링 (2) - optional

헬퍼 메소드를 통해, 라우트의 엔드포인트를 book/{id} -> book/{id}-{title}로 변경 시킬 수 있음.

  • Str::slug() 메소드는 주어진 문자열로부터 URL에 알맞은 “slug”를 생성합니다
use Illuminate\Support\Str;

public function path()
{
  return '/books/' . $this->id . '-' . Str::slug($this->title);
}

엔드포인트 재 설정

Route::patch('/books/{book}-{slug}', 'BooksController@update');

그러나, 업데이트 부분에서 여전히 변경 전 title을 url에 반영하고 있기 때문에 에러가 발생함. 따라서 새롭게 업데이트된 title로 redirect 시키도록, 인스턴스를 새로고침하는 fresh() 메소드를 추가.

$book = Book::first();

$response = $this->patch($book->path(), [
  'title' => 'New Title',
  'author' => 'victor',
]);

$this->assertEquals('New Title', Book::first()->title);
$this->assertEquals('victor', Book::first()->author);
// fresh() 추가
$response->assertRedirect($book->fresh()->path());

BookReservationTest.php 파일을 BookManagementTest.php파일로 이름 변경