LaravelでMutatorsにCarbonを使うときのPHPUnit testメソッドの書き方

carbonでset attributeを使うときは、modelでsetDobAttributeとし、testメソッドでは、$this->assertInstanceOf(Carbon::class, … と書く

## make:test
$ php artisan make:test AuthorManagementTest

テスト駆動開発でも、DBのリレーション関係に沿って開発するのは変わらない

use RefreshDatabase;

    public function test_an_author_can_be_created(){

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

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

$ phpunit –filter test_an_author_can_be_created

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

$ php artisan make:controller AuthorsController
AuthorsController.php

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

$ php artisan make:model Author -m
Athor.php

class Author extends Model
{
    //
    protected $fillable = ['name', 'dob'];
}

migration file

Schema::create('authors', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->timestamp('dob')
            $table->timestamps();
        });
use Carbon\Carbon;
use App\Author;
public function test_an_author_can_be_created(){

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

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

$ phpunit –filter test_an_author_can_be_created
Failed asserting that ’05/14/1988′ is an instance of class “Carbon\Carbon”.

Author.php

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

        $this->withoutExceptionHandling();
        $this->post('/authors', [
            'name' => 'Author Name',
            'dob' => '05/14/1988'
        ]);
        $author = Author::all();

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

$ phpunit –filter test_an_author_can_be_created
OK (1 test, 3 assertions)

git add .
git commit -m “add author”

Test methodというより、mutatorsのcarbonの使い方の方が勉強になりました。

PHPUnit Deleteとモデルのpath()の書き方

deleteのテストメソッドでは、deleteやdestroyではなくassertCountで確認するので注意が必要

### PHPUnit delete
/tests/Feature/BookReservationTest.php

public function test_a_book_can_be_deleted(){

        $this->withoutExceptionHandling();
        
        $this->post('/books', [
            'title' => 'Cool Title',
            'author' => 'Victor'
        ]);

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

        $response = $this->delete('/books/'. $book->id);
        $this->assertCount(0, Book::all());
     $response->assertRedirect('/books');
    }

$ phpunit –filter test_a_book_can_be_deleted

route

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

BooksController.php

public function destroy(Book $book){
    	$book->delete();
    	return redirect('/books');    	
    }

$ phpunit –filter test_a_book_can_be_deleted
OK (1 test, 2 assertions)

### modelによるpath()の関数化
Book.php

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

BooksController.php

return redirect($book->path());

BookReservationTest.php
// test methodも同様に使う

public function testa_book_can_be_added_to_the_library(){
        

        $this->withoutExceptionHandling();

        $response = $this->post('/books', [
            'title' => 'Cool book Title',
            'author' => 'victor',
        ]);

        $book = Book::first();

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

    }

‘/${dirName}/’ . $this->id はよく使う書き方なので、modelにまとめるとすっきりする

Laravel PHPUnit Validation & Update

### validation check
BookReservationTest.php

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

        $response = $this->post('/books', [
            'title' => '',
            'author' => 'victor',
        ]);

        $this->assertSessionHasErrors('title');

    }

BooksController.php

public function store(){
    	$data = request()->validate([
	    	'title' => 'required',
	    ]);
    	Book::create($data);
    }

BookReservationTest.php

public function test_a_title_is_require(){

        $response = $this->post('/books', [
            'title' => '',
            'author' => 'victor',
        ]);

        $response->assertSessionHasErrors('title');

    }

$ phpunit –filter test_a_title_is_require
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 220 ms, Memory: 20.00 MB

OK (1 test, 2 assertions)

titleと同様にauthorもテストする

public function test_a_author_is_require(){

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

        $response->assertSessionHasErrors('author');

    }

### Update method check

public function test_a_book_can_be_updated(){
        $this->post('/books', [
            'title' => 'Cool Title',
            'author' => 'Victor',
        ]);

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

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

    }

$ phpunit –filter test_a_book_can_be_updated
1) Tests\Feature\BookReservationTest::test_a_book_can_be_updated
Failed asserting that two strings are equal.
— Expected
+++ Actual
@@ @@
-‘New Titile’
+’Cool Title’

route

Route::patch('/books/{book}', 'BooksController@update');
public function test_a_book_can_be_updated(){
        $this->withoutExceptionHandling();
        $this->post('/books', [
            'title' => 'Cool Title',
            'author' => 'Victor'
        ]);

        $book = Book::first();

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

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

    }
public function update(Book $book){

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

リファクタリング

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

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

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

$ git add .
$ git commit -m “php unit”

Laravelの書き方に沿ってます。こりゃフレームワークの書き方がわからずにテスト工程をコントロールするのは若干無理がありますな。

Test Driven Development(テスト駆動開発) in Laravel

最初にテストコードを書き、テストコードに適用するように毎回phpunitを実行しながら実装していく手法

### first step to do
$ composer create-project –prefer-dist laravel/laravel library
$ cd library
$ php artisan –version
$ ls -al
$ git init
$ git status
$ git add .
$ git commit -m “initial commit”

.env

DB_CONNECTION=sqlite

$ touch database/database.sqlite

phpunit.xml

<php>
        <server name="APP_ENV" value="testing"/>
        <server name="BCRYPT_ROUNDS" value="4"/>
        <server name="CACHE_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
        <server name="MAIL_DRIVER" value="array"/>
        <server name="QUEUE_CONNECTION" value="sync"/>
        <server name="SESSION_DRIVER" value="array"/>
        <server name="DB_CONNECTION" value="sqlite"/>
        <server name="DB_DATABASE" value=":memory:"/>
    </php>

tests/Feature/ExampleTest.php
tests/Feature/BookReservationTest.php

namespace Tests\Feature;

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

class BookReservationTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}
class BookReservationTest extends TestCase
{
    public function testa_book_can_be_added_to_the_library(){
        
        $this->withoutExceptionHandling();

        $response = $this->post('/books', [
            'title' => 'Cool book Title',
            'author' => 'victor',
        ]);
        $response->assertOk();

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

    }
}

$ phpunit –filter testa_book_can_be_added_to_the_library
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

F 1 / 1 (100%)

Time: 163 ms, Memory: 16.00 MB

There was 1 failure:

1) Tests\Feature\BookReservationTest::testa_book_can_be_added_to_the_library
Response status code [404] does not match expected 200 status code.
Failed asserting that false is true.

/home/vagrant/library/vendor/laravel/framework/src/Illuminate/Foundation/Testing/TestResponse.php:88
/home/vagrant/library/tests/Feature/BookReservationTest.php:15

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

route

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

$ phpunit –filter testa_book_can_be_added_to_the_library
1) Tests\Feature\BookReservationTest::testa_book_can_be_added_to_the_library
Illuminate\Contracts\Container\BindingResolutionException: Target class [App\Http\Controllers\BooksController] does not exist.

$ php artisan make:controller BooksController

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

$ phpunit –filter testa_book_can_be_added_to_the_library
1) Tests\Feature\BookReservationTest::testa_book_can_be_added_to_the_library
Error: Class ‘Tests\Feature\Book’ not found

$ php artisan make:model Book -m

namespace App\Http\Controllers;

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

class BooksController extends Controller
{
    //
    public function store(){
    	Book::create([
    		'title' => request('title'),
    		'author' => request('author')
    	]);
    }
}

Book.php

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

migration file

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

>>> \Schema::getColumnListing(‘books’);
=> [
“id”,
“title”,
“author”,
“created_at”,
“updated_at”,
]

class BookReservationTest extends TestCase
{

    use RefreshDatabase;
    
    public function testa_book_can_be_added_to_the_library(){
        

        $this->withoutExceptionHandling();

        $response = $this->post('/books', [
            'title' => 'Cool book Title',
            'author' => 'victor',
        ]);
        $response->assertOk();

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

    }
}

$ phpunit –filter testa_book_can_be_added_to_the_library
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

. 1 / 1 (100%)

Time: 223 ms, Memory: 20.00 MB

OK (1 test, 2 assertions)

テストコードを正確に書ければ、phpunitによるメッセージに沿って順番に進めることができる。
ただ、これ、migrationfile, route, model, controller書くだけになのに、時間かかりすぎるな。手が空いている時などはいいかもしれないが、毎回これやってたら開発効率が極端に落ちるように思うので、ケースバイケースだが、積極的には採用しずらい。

Laravel PHPUnitとは?

– ドメインロジックに対してテストを行う
– vendor/bin/phpunit –filter ${methodName}でテスト実行
– test用のDBをテスト毎にリフレッシュしてテストしている
 – > use Illuminate\Foundation\Testing\RefreshDatabase;

$ php artisan –version
Laravel Framework 6.9.0

$ vendor/bin/phpunit
PHPUnit 8.5.0 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 310 ms, Memory: 16.00 MB

OK (2 tests, 2 assertions)

tests/Feature/ExampleTest.php

namespace Tests\Feature;

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

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        $response = $this->get('/');

        $response->assertStatus(200);
    }
}
public function a_new_user_gets_an_email_when_it_register(){
        
    }

### 未ログインユーザーのリダイレクトテスト
tests/Feature/CustomersTest.php

class CustomersTest extends TestCase
{
    public function only_logged_in_users_can_see_the_customers_list(){
        $response = $this->get('/customers')->assertDedirect('/login');
    }
}

$ vendor/bin/phpunit –filter only_logged_in_users_can_see_the_customers_list

### ログインユーザーのリダイレクトテスト

class CustomersTest extends TestCase
{
    use RefreshDatabase;
    
    public function only_logged_in_users_can_see_the_customers_list(){
        $response = $this->get('/customers')->assertDedirect('/login');
    }

    public function authenticated_users_can_see_the_customers_list(){
        $this->actingAs(factory(User::class)->create());

        $response = $this->get('/customers')->assertOk('200');
    }
}

$ vendor/bin/phpunit –filter authenticated_users_can_see_the_customers_list

### store methodのテスト

public function a_customer_can_be_added_through_the_form(){

        $this->withoutExceptionHandling();
        $this->actingAs(factory(User::class)->create([
                'email' => 'admin@admin.com'
        ]));

        $response = $this->post('customers', [
                'name' => 'Test User',
                'email' => 'test@test.com',
                'active' => 1,
                'company_id' = 1

        ]);

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

### validation test

private function data(){
        return [
                'name' => 'Test User',
                'email' => 'test@test.com',
                'active' => 1,
                'company_id' = 1
        ]
    }
public function a_name_is_required(){

        Event::fake();

        $this->actingAs(factory(User::class)->create([
                'email' => 'admin@admin.com'
        ]));

        $response = $this->post('customers', array_merge($this->data(), ['name' => '']));

        $response->assertSessionHasErrors('name');

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

    }

手動で行っているテストをRefreshDatabaseを使ってコードでテストしている。PHPUnitの概念は解った。実務でどこまで細かくやるかだな。全部PHPUnitで確認するのはしんどいが、
間違いが絶対に許されない、かつ、複雑な処理の箇所は、アジャイル開発でもPHPUnitできちんとテストした方がセキュアで良いかもしれません。

Laravel 6.x系のvagrant & homestead導入手順

今、Laravelを開発するなら、6系以外は考えられない
ということで、vagrant & homesteadで環境構築したいと思います。

Laravel Homestead 公式ドキュメント
https://readouble.com/laravel/6.x/ja/homestead.html

$ vagrant -v
Vagrant 1.9.7

$ vagrant box add laravel/homestead

$ git clone https://github.com/laravel/homestead.git Homestead
$ cd homestead
$ ls
bin/ Homestead.yaml.example phpunit.xml.dist src/
CHANGELOG.md init.bat readme.md tests/
composer.json init.sh* resources/ Vagrantfile
composer.lock LICENSE.txt scripts/
$ git checkout release
$ bash init.sh

### homestead.yaml

---
ip: "192.168.10.10"
memory: 2048
cpus: 2
provider: virtualbox

authorize: ~/.ssh/id_rsa.pub

keys:
    - ~/.ssh/id_rsa

folders:
    - map: ~/workspace/homestead/test
      to: /home/vagrant/test

sites:
    - map: homestead.test
      to: /home/vagrant/test/public

databases:
    - homestead

features:
    - mariadb: false
    - ohmyzsh: false
    - webdriver: false

### vagrant再インストール
$ vagrant -v
Vagrant 2.2.6

$ vagrant up
$ vagrant plugin expunge –reinstall
$ vagrant plugin update

$ vagrant up
$ vagrant status
$ vagrant halt
$ vagrant destroy

$ vagrant ssh

Welcome to Ubuntu 18.04.3 LTS (GNU/Linux 4.15.0-72-generic x86_64)

Thanks for using
_ _ _
| | | | | |
| |__ ___ _ __ ___ ___ ___| |_ ___ __ _ __| |
| ‘_ \ / _ \| ‘_ ` _ \ / _ \/ __| __/ _ \/ _` |/ _` |
| | | | (_) | | | | | | __/\__ \ || __/ (_| | (_| |
|_| |_|\___/|_| |_| |_|\___||___/\__\___|\__,_|\__,_|

* Homestead v10.0.0 released
* Settler v9.1.0 released

$ php -v
PHP 7.4.0 (cli) (built: Nov 28 2019 07:27:06) ( NTS )
$ php artisan –version
Laravel Framework 6.9.0
$ git –version
git version 2.17.1
$ node -v
v12.13.1

こいつは強力!!!
あれ、でもこれ、EC2にデプロイする際には、EC2でのミドルウェアインストールは必須な訳でしょ。。

そう考えると、本当に時間がない時などに限られるか。

Amazon Linux 2のVagrant boxでshutdownできない時

vagrant boxesからamazon linux 2を入れて、環境構築をする
https://app.vagrantup.com/gbailey/boxes/amzn2

>mkdir amzn2
>cd amzn2
>vagrant init gbailey/amzn2

Vagrantfile

config.vm.network "private_network", ip: "192.168.33.10"

>vagrant up
>vagrant status
>vagrant ssh
$ exit

>vagrant halt

>vagrant status
default running (virtualbox)

なにいいいいいいいいいいいいいいいいいいいい?

Vagrantfile
config.vm.guest = :amazonを追加

config.vm.box = "gbailey/amzn2"
  config.vm.guest = :amazon

>vagrant halt
>vagrant status
default poweroff (virtualbox)

beautiful!

vagrant awslinuxとLarave update6.0

– php7.1は2019/12より積極的にメンテナンスされなくなる
– その為、php7.2以上が要求

$ php -v
PHP 7.1.7 (cli) (built: Sep 14 2017 15:47:38) ( NTS )
$ cat /etc/issue
Amazon Linux AMI release 2017.03

### PHP7.4へのアップデート
// repositoryのインストール
$ sudo yum install -y https://rpms.remirepo.net/enterprise/remi-release-7.rpm
$ sudo yum install -y –enablerepo=remi-php74 php which
Loaded plugins: priorities, update-motd, upgrade-helper
Error getting repository data for remi-php74, repository not found

あれ、vagrantboxesのawslinuxだと、php7.1以上は入れられない?
ってことは、vagrant init は、mvbcoding/awslinuxではなく、centos/7でやらないと駄目ってこと??
今のタイミングでlaravel6.0以外で開発するってのは絶対ありえんからなー

$ exit
logout
Connection to 127.0.0.1 closed.
> vagrant halt

Laravel update 5.7&5.8

### laravel 5.7
upgradeページを確認
https://readouble.com/laravel/5.7/ja/upgrade.html

composer.json

"require": {
        "php": ">=7.1.3",
        "laravel/framework": "5.7.*",
        "laravelcollective/html": "5.7.*",
        "cviebrock/eloquent-sluggable": "^4.3",
        "unisharp/laravel-filemanager": "^1.9",
        "intervention/image": "^2.5"
    },

$ php composer.phar update
$ php artisan –version
->エラー
-> Unresolvable dependency resolving [Parameter #0 [ $app ]] in class Illuminate\Support\Manager

config/app.php

Illuminate\Notifications\NotificationServiceProvider::class,

$ php artisan –version

### laravel 5.8
https://readouble.com/laravel/5.8/ja/upgrade.html

composer.json

"require": {
        "php": ">=7.1.3",
        "laravel/framework": "5.8.*",
        "laravelcollective/html": "5.8.*",
        "cviebrock/eloquent-sluggable": "^4.3",
        "unisharp/laravel-filemanager": "^1.9",
        "intervention/image": "^2.5"
    },

$ php artisan –version

criticalな変更はそこまでありませんが、公式のupgrade guideを読むと、セキュリティパッチが多く、やはり最新版に保った方が良さそうだ。

Laravel JavasScriptへデータを渡す書き方

HTML同様、scriptタグ内でも{{$variable}}と書くとデータが渡る

$ php artisan make:controller AdminController

web.php

Route::get('/', 'HomeController@index');
Route::get('/admin', 'AdminController@index');

AdminController.php

class AdminController extends Controller
{
    //
    public function index(){
    	return view('admin/index');
    }
}

AdminController.php

public function index(){
    	$postsCount = Post::count();
    	$categoriesCount = Category::count();
    	$commentsCount = Comment::count();

    	return view('admin/index',compact('postsCount','categoriesCount','commentsCount'));
    }

index.blade.php

<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.3/dist/Chart.min.js"></script>
<script>
var ctx = document.getElementById('myChart').getContext('2d');
var myChart = new Chart(ctx, {
    type: 'bar',
    data: {
        labels: ['Posts', 'Categories', 'Comments'],
        datasets: [{
            label: 'Data of CMS',
            data: [{{$postsCount}},{{$categoriesCount}},{{$commentsCount}}],
            backgroundColor: [
                'rgba(255, 99, 132, 0.2)',
                'rgba(54, 162, 235, 0.2)',
                'rgba(255, 206, 86, 0.2)',
                'rgba(75, 192, 192, 0.2)',
                'rgba(153, 102, 255, 0.2)',
                'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
                'rgba(255, 99, 132, 1)',
                'rgba(54, 162, 235, 1)',
                'rgba(255, 206, 86, 1)',
                'rgba(75, 192, 192, 1)',
                'rgba(153, 102, 255, 1)',
                'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
        }]
    },
    options: {
        scales: {
            yAxes: [{
                ticks: {
                    beginAtZero: true
                }
            }]
        }
    }
});
</script>

controllerのdataの取得は、count()以外にも、JSで表現したい内容に合わせてControllerで取得・整形する
JS側ではなくController側で整えるのが一般的か