해리의 데브로그

Test Driven Laravel 02 - Book:Update 테스트 코드 구현 / 리팩토링

|

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

1. Update 테스트 코드 구현

데이터를 업데이트 시키는 기본 코드 작성

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

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

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

테스트를 동작시키면, 라우트에 대하여 PATCH가 지원되지않는다는 에러 발생. 따라서 라우트를 추가시킴.

//Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException : The PATCH method is not supported for this route. Supported methods: POST.
  
Route::patch('/books', 'BooksController@update');

컨트롤러에 update 메소드 기본 코드 작성.

public function update()
{
  $data = request()->validate([
    'title'=>'required',
    'author'=> 'required',
  ]);
}

하지만, 현재 코드로도 여전히 에러가 발생한다. 업데이트 로직을 구현하기 위해서는 어떤 데이터에 대하여 업데이트를 할 건지를 명시해줘야한다. 이때 데이터의 id값을 사용.

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

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

  // book 클래스 인스턴스 추가
  $book = Book::first();  

  //id값을 variable routing으로 사용
  $response = $this->patch('/books/' . $book->id, [ 
    'title' => 'New Title',
    'author' => 'Victor',
  ]);

라우트와 컨트롤러를 적절히 수정시킴.

Route::patch('/books/{book}', 'BooksController@update');
public function update(Book $book)  // route model binding
{
  $data = request()->validate([
    'title'=>'required',
    'author'=> 'required',
  ]);
  
  $book->update($data);
}

2. Create & Update 테스트 코드 및 로직 구현

결과적으로, 데이터를 생성하고 업데이트하는 로직을 성공적으로 아래와 같이 구현 완료

  • 데이터 생성
  • 제목, 작가명 유효성 검증
  • 데이터 업데이트
// BooksController.php
namespace App\Http\Controllers;

use App\Book;
use Illuminate\Http\Request;

class BooksController extends Controller
{
    public function store()
    {
        $data = request()->validate([
            'title'=>'required',
            'author'=> 'required',
        ]);

        Book::create($data);
    }

    public function update(Book $book)
    {
        $data = request()->validate([
            'title'=>'required',
            'author'=> 'required',
        ]);

        $book->update($data);
    }
}
//web.php

Route::post('/books', 'BooksController@store');
Route::patch('/books/{book}', 'BooksController@update');
// BookManagementTest.php
namespace Tests\Feature;

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

class BookReservationTest extends TestCase
{
    use refreshDatabase;

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

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

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

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

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

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

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

        $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);
    }
}

3. 리팩토링

유효성 검증하는 코드가 반복적으로 사용되고 있음. 이 코드를 extract 시켜 별도의 메소드로 구현

$data = request()->validate([
  'title'=>'required',
  'author'=> 'required',
]);
public function store()
{
  $data = $this->validateRequest();

  Book::create($data);
}

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

  $book->update($data);
}

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

$data를 인라인으로 변경하는 것도 가능

// before
$data = $this->validateRequest();
$book->update($data);

// after 
$book->update($this->validateRequest());

리팩토링 최종 완성된 코드는 다음과 같음.

class BooksController extends Controller
{
    public function store()
    {
        Book::create($this->validateRequest());
    }

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

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

Test Driven Laravel 01 - GET STARTED / Book:Create 테스트 코드 구현

|

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

1. PHPUnit Setup

.env에서 데이터베이스를 sqlite로 설정

DB_CONNECTION=sqlite

sqlite 데이터베이스 생성

touch database/database.sqlite 

2. Create(store) 테스트 코드 구현

Tests/Feature/BookReservationTest.php 생성 후, 데이터를 추가하는 기본코드 작성

  • php artisan make:test bookReservationTest
class BookReservationTest extends TestCase
{
    /** @test */
    public function a_book_can_be_added_to_the_library()
    {
        $response = $this->post('/books', [
            'title' => 'cool book title',
            'author' => 'harry'
        ]);

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

withoutExceptionHandling 는 phpuit test 결과로 어떠한 에러가 발생했는지를 알려줌.

$this->withoutExceptionHandling();

라우터 및 컨트롤러 생성

Route::post('/book', 'BooksController@store');

php artisan make:controller BooksController

데이터를 생성하여 저장시키기 위해, 컨트롤러 내 store 메소드생성

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

모델 생성 & 마이그레이션 적용. 이후, BookReservationTest.php 및 BooksController 에 모델 Book을 import해줌.

php artisan make:model Book -m

또한, use refreshDatabase; 를 테스트 파일 내부에서 import시킴. 이 코드는 테스트를 동작시킬 때 마다 데이터베이스를 초기화 시키는 역할을 함.

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

class BookReservationTest extends TestCase
{
    use refreshDatabase;

모델에 mass assignment 설정

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

마이그레이션에 title, author 추가

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

3. 유효성 검증 테스트 코드 작성

제목 유효성 검증에 대한 테스트케이스 작성

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

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

컨트롤러에 유효성 검증에 대한 코드 추가

class BooksController extends Controller
{
    public function store()
    {
        $data = request()->validate([
            'title'=>'required',
            'author'=> 'required'
        ]);

        Book::create($data);
    }
}

모든 프로세스가 완료 되었으나, withoutExceptionHandling() 로 에러메세지가 반환되므로 이 부분은 주석처리

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

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

마찬가지로, 작가명이 누락된 테스트케이스도 작성 가능

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

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

The PHP Practitioner 11 - Array Filtering

|

Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.

0. GET STARTED

기본 코드는 다음과 같음

class Post
{
    public $title;
    public $author;
    public $published;

    public function __construct($title, $author, $published)
    {
        $this->title = $title;
        $this->author = $author;
        $this->published = $published;
    }
}

$posts = [
    new Post('My first post', 'harry', true),
    new Post('My second post', 'harry',  true),
    new Post('My third post', 'mat', true),
    new Post('My fourth post', 'simon', false),
];

PHP에서 array를 활용할 수 있는 메서드들이 매우 많음. 그 중 대표적인 것이 아래.

1. array-filter

filter down the array - array_filter(array, closure function)

$unpublishedPosts = array_filter($posts, function ($post) {
    // return $post -> published === false;
    return ! $post -> published;

});

$publishedPosts = array_filter($posts, function ($post) {
    return $post -> published;

});

var_dump($unpublishedPosts);
var_dump($publishedPosts);

2. array_map

map over the array(change the status). It is useful when you need to modify what gets refunded

  • array_map(closure function, array)
// map over the array -> change the status - array_map(closure, array)
// when you need to modify what gets returned.
$modified = array_map(function($post) {
    $post->published = true;
    return $post;
}, $posts);

var_dump($modified);

// 응용 1. (array) -> return each element as array ( this case, obj -> array)
$modified = array_map(function($post){
    return (array) $post;

}, $posts);

var_dump($modified);

// 응용 2.
$modified = array_map(function($post){
    return ['title' => $post->title];
}, $posts);

var_dump($modified);

// 응용 3. map 메서드는 foreach 로도 표현 가능
foreach ($posts as $post) {
    $post->published = true;
}

var_dump($posts);

3. array_column

extract specific column from the array(object)

  • array_column(array, column)
// 특정 컬럼만을 추출할 때 유용. 오브젝트, 배열 모두에 사용 가능
$titles = array_column($posts, 'title');

// 응용) 3번째 인자로, key 값에 해당하는 컬럼을 지정할 수 있음.
// 'my first post' : "harry"
$titles = array_column($posts, 'author', 'title');

var_dump($titles);

The PHP Practitioner 10 - View

|

Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.

1. Template 상속

views/partial : 재사용가능한 template(HTML)을 추출하여 partial 폴더에서 관리할 수 있음.

  • <?php require('partials/nav.php); ?>
  • DJango에서 템플릿 확장/상속(base template)과 같은 개념임.
<body>

<?php require('partials/nav.php'); ?>
<nav>
    <ul>
        <li><a href="/about">About Page</a></li>
        <li><a href="/about/culture">About our Culture</a></li>
        <li><a href="/contact">Contact Page</a></li>
    </ul>
</nav>

마찬가지로, partial의 개념을 이용하여 head, footer의 재사용되는 코드를 따로 빼서 새로운 php 파일로 관리한 후 그 파일을 불러오는 방식으로 어플리케이션 구성을 다듬을 수 있다.

  • partials 폴더 내 head.php, footer.php 생성
  • head.php내 포함된 nav.php 경로는 상대경로이므로 수정
  • CSS는 public/css/style.css를 새로 만들어 관리
// head.php
<!<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="/public/css/style.css">
</head>
<body>

<?php require('nav.php'); ?>
  
// footer.php
</body>
</html>
  
// about.view.php
<?php require('partials/head.php') ?>
<h1>About Us</h1>
<?php require('partials/footer.php') ?> 

The PHP Practitioner 09 - Router

|

Laracasts - The PHP Practitioner 강의를 듣고 정리한 포스팅 입니다.

1. Router

어플리케이션의 url을 관장하는 역할(Django의 urls.py). 어플리케이션의 구조를 아래와 같이 좀 더 구체화시킴

  • Views: Template의 역할. 브라우저에 보여지는 부분
  • Controllers: router에 따라 연결된 controller를 호출하며 각 controller는 정해진 로직에 따라 views와 연결.
  • Core: Database, boostrap, 추가적인 클래스 정의

routes.php: root directory에 생성. 연습을 위해 “about”, “about-culture”, “contact”, “index” 페이지에 대한 views, controller, routes를 형성함.

1-1. Controllers & views의 예시
// controllers/about.php
<?php

require 'views/about.view.php';

//views/about.php
<body>
<h1>About Us</h1>
</body>

2. Routes.php

<?php

$router->define([
    '' => 'controllers/index.php',
    'about' => 'controllers/about.php',
    'about/culture' => 'controllers/about-culture.php',
    'contact' => 'controllers/contact.php'
]);

//아래와 같이 다양한 방법으로도 표현 가능

//$router->define('', 'controllers/index.php');
//$router->define('about', 'controllers/index.php');
// Router::define('', 'controllers/index.php');
//$router->define('', 'controllers/index.php');

3. core/bootstrap.php

router 관련 로직을 core/Router.phprouter 클래스에서 구현 & bootstrap.php 와 연결

  • Request.php : 서버 내 요청된 URItrim하는 클래스가 구현된 파일
<?php

$app = [];

$app['config'] = require 'config.php';

require 'core/Router.php';
require 'core/Request.php';
require 'core/database/Connection.php';
require 'core/database/QueryBuilder.php';

$app['database'] =  new QueryBuilder(
    Connection::make($app['config']['database'])
);

4. core/Router.php

define : $routes 를 프로퍼티에 저장하는 메서드

load : $fileroutes.php 의미함. routes.php 의 로직에 따라 ($router->define([])) 의 routing값들이 $routes 에 저장됨.

  • load 는 스태틱 메서드이므로 인스턴스를 생성하지 않음. 따라서, new static 또는 new self 를 사용해야함.
  • 인스턴스를 생성하지 않으므로 $this 는 사용이 불가능함. 따라서, new static 이 저장된 $router 을 return 함.

direct : 들어온 URI가 실제로 존재하는지 유무를 프로퍼티와 대조하여 검증함 (array_key_exists) . 존재할 경우, 대응되는 URI를 반환시킴. 존재하지 않을 경우 Throw

<?php

class router
{
    protected $routes = [];

    public static function load($file) // $file: routes.php
    {
        // static method는 인스턴스를 생성하지 않는 global method.
        // 인스턴스 생성을 위해서는 new static (or new self)
        $router = new static;

        require $file;

        // return $this; static method는 인스턴스를 생성하지 않으므로 $this는 사용이 불가능 함.
        return $router;

    }

    public function define($routes)
    {
        $this->routes = $routes;
    }

    public function direct($uri)
    {
        // about/culture
        // 들어온 uri와 매칭되는 key값이 있는지를 routes.php에서 검
        if (array_key_exists($uri, $this->routes)) {
            return $this->routes[$uri];
        }

        // 존재하지 않을 경우 Throw
        throw new Exception('No route defined for this uri');
    }

5. Request.php

<?php

class request
{
    public static function uri()
    {
        return trim($_SERVER['REQUEST_URI'],'/');
    }
}

6. index.php

bootstrap.php 를 호출함으로써, router, request 클래스 등 router에 필요한 모든 로직을 불러올 수 있음.

<?php

require 'core/bootstrap.php';

// option 1.
$router = new Router; // Router 클래스는 bootstrap과 연결되어 있음.
require 'routes.php'; // routes (uri list)를 불러오며 $route(route 인스턴스)의 프러퍼티에 경로 저장.

$uri = trim($_SERVER['REQUEST_URI'],'/'); // URI 경로 trim

require $router->direct($uri); // Router 클래스의 메서드: routes 존재 여부 검사후 redirect.

//die(var_dump($app));


// option 2.
$router = Router::load('routes.php'); 
require $router-> direct($uri);


// option 3.
require Router::load('routes.php')
    ->direct(Request::uri());