amazon linux2(vagrant) + Laravel 7.xでDuskを使うまでの手順

Laravelでseleniumを使おうと思ったら、duskというパッケージがあるらしいので、duskをインストールして使っていきます。環境は、vagrant、amazon linux2, laravel 7.2.1です。

### 1.duskインストール
$ php composer.phar require –dev laravel/dusk
– laravel/dusk v5.10.0 requires ext-zip * -> the requested PHP extension zip is missing from your system.

$ sudo yum install php-pecl-zip.x86_64
$ php composer.phar require –dev laravel/dusk

$ php artisan dusk:install
testsディレクトリにBrowserディレクトリが作られる

.env

APP_URL=http://192.168.33.10:8000

### 2.duskテスト
$ php artisan dusk:make LoginTest

/tests/Browser/LoginTest.php

public function test_can_login_successfull(){
        $this->browse(function (Browser $browser) {
            $browser->visit('/login')
                ->type('email', 'foo@hoge.com')
                ->type('password', 'hogehoge')
                ->press('Login')
                ->assertSee('this is subscriber');
        });
    }
$ php artisan dusk
1) Tests\Browser\ExampleTest::testBasicExample
Facebook\WebDriver\Exception\WebDriverCurlException: Curl error thrown for http POST to /session with params: {"capabilities":{"firstMatch":[{"browserName":"chrome","goog:chromeOptions":{"binary":"","args":["--disable-gpu","--headless","--window-size=1920,1080"]}}]},"desiredCapabilities":{"browserName":"chrome","platform":"ANY","chromeOptions":{"binary":"","args":["--disable-gpu","--headless","--window-size=1920,1080"]}}}

Failed to connect to localhost port 9515: Connection refused

ん? ChromeDriverが入っていない?

### 3.ChromeDriver インストール
$ ./vendor/laravel/dusk/bin/chromedriver-linux –verbose
./vendor/laravel/dusk/bin/chromedriver-linux: error while loading shared libraries: libX11.so.6: cannot open shared object file: No such file or directory
$ sudo yum install -y libX11
$ ./vendor/laravel/dusk/bin/chromedriver-linux –v
ChromeDriver 80.0.3987.106 (f68069574609230cf9b635cd784cfb1bf81bb53a-refs/branch-heads/3987@{#882})

$ php artisan dusk
1) Tests\Browser\ExampleTest::testBasicExample
Facebook\WebDriver\Exception\UnknownErrorException: unknown error: cannot find Chrome binary

なに、今度はChrome binary?

### 4.Chrome install
$ curl https://intoli.com/install-google-chrome.sh | bash
$ php artisan dusk

1) Tests\Browser\LoginTest::test_can_login_successfull
Facebook\WebDriver\Exception\NoSuchElementException: no such element: Unable to locate element: {"method":"css selector","selector":"body textarea[name='email']"}
  (Session info: headless chrome=80.0.3987.149)

どういうこと? 

### 5.visitでパスを直接指定

public function test_can_login_successfull(){
        $this->browse(function (Browser $browser) {
            $browser->visit('http://192.168.33.10:8000/login')
                // ->dump();
                ->type('email', 'foo@hoge.com')
                ->type('password', 'hogehoge')
                ->press('Login')
                ->assertSee('this is subscriber');
        });

    }
$ php artisan dusk
PHPUnit 8.5.2 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 2.18 seconds, Memory: 18.00 MB

OK (1 test, 1 assertion)

上手くいきました。半日かかりましたが、seleniumが何やってるかは理解できました。

Laravel7.x : Expected status code 200 but received 302.

Circle CIでテスト

There was 1 failure:

1) Tests\Feature\ExampleTest::testBasicTest
Expected status code 200 but received 302.
Failed asserting that false is true.

/home/circleci/repo/vendor/laravel/framework/src/Illuminate/Testing/TestResponse.php:185
/home/circleci/repo/tests/Feature/ExampleTest.php:19

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

リダイレクト処理で302になっている為。
tests/Feature/ExampleTest.php

    public function testBasicTest()
    {
        $response = $this->get('/');

        // $response->assertStatus(200);
        $response->assertStatus(302);
    }

-> SUCCESS

OK^^

wip TDD、tailwindcss, @errorの書き方

wip: work in progress

refactoring

public function test_an_author_can_be_created(){

        $this->post('/authors', $this->data());
        $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'));
    }

    private function data(){
        return [
            'name' => 'Author Name',
            'dob' => '05/14/1988'
        ];
    }

$ phpunit –filter test_an_author_can_be_created

public function test_a_name_is_required(){
        $response = $this->post('/authors', array_merge($this->data(), ['name' => '']));

        $response->assertSessionHasErrors('name');

    }

    public function test_a_dob_is_required(){
        $response = $this->post('/authors', array_merge($this->data(), ['dob' => '']));
        $response->assertSessionHasErrors('dob');

    }

controller

public function store(){
    	$data = $this->validateData();

    	Author::create($data);
    }

    protected function validateData(){
    	return request()->vadidate([
    		'name' => 'required',
    		'dob'=> '',
    	]);
    }

### tailwindcss
$ php artisan help preset
$ php artisan preset none
$ php artisan preset vue

$ npm install

https://tailwindcss.com/
$ npm install tailwindcss –save-dev

app.scss

@tailwind base;

@tailwind components;

@tailwind utilities;

$ npx tailwind init

webpack.mix.js

const tailwindcss = require('tailwindcss');
mix.js('resources/js/app.js', 'public/js')
   .sass('resources/sass/app.scss', 'public/css')
   .options({
    processCssUrls: false,
    postCss: [ tailwindcss('./.config.js') ],
  });

$ npm install webpack

$ npm run dev

authors/create.blade.php

@extends('layouts.app')

@section('content')
	<dvi class="bg-gray-300 h-screen">
		asdfg
	</dvi>
@endsection

web.php

Route::get('/authors/create', 'AuthorsController@create');

create.blade.php
test

@extends('layouts.app')

@section('content')
	<div class="w-2/3 bg-gray-200 mx-auto">
		asdfg
	</div>
@stop

### @errorの
styling

<div class="w-2/3 bg-gray-200 mx-auto p-6 shadow">
		<form action="/authors" method="post" class="flex flex-col items-center">
			@csrf
		<h1>Add New Author</h1>
		<div class="pt-4">
			<input type="text" name="name" placeholder="Full Name" class="rounded px-4 py-2 w-64">
			@if{{$errors->has('dob')}}
				<p class="text-red-600">{{$errors->first('name')}}</p>
			@endif
		</div>
		<div class="pt-4">
			<input type="text" name="dob" placeholder="Date of Birth" class="rounded px-4 py-2 w-64">
			@if{{$errors->has('dob')}}
				<p class="text-red-600">{{$errors->first('dob')}}</p>
			@endif
		</div>
		<div class="pt-4">
			<button class="bg-blue-400 text-white rounded py-2 px-4">Add New Author</button>
		</div>
		</form>
	</div>
@error('name') <p class="text-red-600">{{$message}}</p> @enderror

tailwindはモダンな感じがする
しかし、6系になってフロント周りを中心に随分仕様が変わってます。

laravel feature test 404と6系のmake:auth

– laravel 6系ではmake:authが無くなりました
$ php artisan –version
Laravel Framework 6.9.0
– unit test -> feature testの順番

$ php artisan make:test BookCheckoutTest

use RefreshDatabase;
    public function test_a_book_can_be_check_out(){

        $this->withoutExceptionHandling();

        $book = factory(Book::class)->create();
        $this->actingAs($user = Factory(User::class)->create())
            ->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);
    }

route

Route::post('/checkout/{book}', 'CheckoutBookController@store');

$ php artisan make:controller CheckoutBookController

public function __construct(){
		$this->middleware('auth');
	}
public function store(Book $book){
    	$book->checkout(auth()->user());
    }

$ phpunit –filter test_a_book_can_be_check_out
OK (1 test, 4 assertions)

$ php artisan make:auth
Command “make:auth” is not defined.

### make:auth
$ composer require laravel/ui –dev
$ php artisan ui vue –auth
$ php artisan migrate:refresh
$ npm install

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

$ php artisan make:controller CheckinBookController

public function __construct(){
		$this->middleware('auth');
	}
    public function store(Book $book){
    	$book->checkin(auth()->user());
    }
public function test_404_is_thrown_if_a_book_is_not_checked_out(){

        $book = factory(Book::class)->create();
        $user = factory(User::class)->create();

        $this->actingAs($user)
            ->post('/checkin/' . $book->id)
            ->assertStatus(404);

        $this->assertCount(0, Reservation::all());
    }
public function __construct(){
		$this->middleware('auth');
	}
    public function store(Book $book){

    	try {
    		$book->checkin(auth()->user());
    	} catch(\Exception $e){
    		return response([], 404);
    	}
    	
    }

make:authがvueのコンポーネントになったということは、LaravelはReactよりvueを支持しているってこと?フロントエンドは確実にvue.jsが必須になってきました。

それと、テストエンジニアってなんか敬遠されがちだが、そもそもtestはフレームワークの仕組みを深く理解してないとできないから、テストエンジニアを下に見るのは偏見がかなり入っている

Laravel hasManyのUnitTest

– 想定する条件において、親子の各テーブルをassertCount, assertEquals、assertNotNullでcheckする
– feature testとunit testの違い
 -> userの挙動のテストがfeature test, unit testはuser操作が関係ない
– unit testのデータ生成にはfactoryを活用

$ php artisan make:test BookReservationsTest

namespace Tests\Unit;

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

class BookReservationsTest extends TestCase
{
    
    use RefreshDatabase;

    public function test_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);
    }
}

$ phpunit –filter test_a_book_can_be_checked_out
Tests\Unit\BookReservationsTest::test_a_book_can_be_checked_out

$ php artisan make:factory BookFactory -m Book
BookFactory.php

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),
    ];
});

$ phpunit –filter test_a_book_can_be_checked_out

$ php artisan make:factory AuthorFactory -m Author

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

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

Book.php

public function checkout(){
    	
    }

$ php artisan make:model Reservation -m

Book.php

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

    public function reservations(){
    	return $this->hasMany(Reservation::class);	
    }

Reservation.php

protected $fillable = [
        'user_id',
        'book_id',
        'checked_out_at'
    ];

migration file

Schema::create('reservations', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('user_id');
            $table->unsignedBigInteger('book_id');
            $table->timestamp('checked_out_at');
            $table->timestamp('checked_in_at');
            $table->timestamps();
        });

$ phpunit –filter test_a_book_can_be_checked_out

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

        $book->checkin();

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

    }

Book.php

public function checkin($user){
    	$reservation = $this->reservations()->where('user_id', $user->id)->whereNotNull('checked_out_at')->whereNull('checked_in_at')->first();

    	$reservation->update([
    		'checked_in_at' => now(),
    	]);
    }
public function test_a_book_can_be_returned(){
        $book = factory(Book::class)->create();
        $user = factory(User::class)->create();

        $book->checkout($user);
        $book->checkin($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_in_at);

    }

    public function test_a_user_can_checkout_a_book_twice(){

        $book = factory(Book::class)->create();
        $user = factory(User::class)->create();
        $book->checkout($user);
        $book->checkin($user);

        $book->checkout($user);

        $this->assertCount(2, Reservation::all());
        $this->assertEquals($user->id, Reservation::find(2)->user_id);
        $this->assertEquals($book->id, Reservation::find(2)->book_id);
        $this->assertNull(Reservation::find(2)->checked_in_at);
        $this->assertEquals(now(), Reservation::find(2)->checked_out_at);

        $book->checkin($user);

        $this->assertCount(2, Reservation::all());
        $this->assertEquals($user->id, Reservation::find(2)->user_id);
        $this->assertEquals($book->id, Reservation::find(2)->book_id);
        $this->assertNotNull(Reservation::find(2)->checked_in_at);
        $this->assertEquals(now(), Reservation::find(2)->checked_in_at);

    }

UnitTestを細かくやる人はこだわりがあり、レベルが高い印象があります。

Laravel OneToOneのテストメソッドの書き方

– 親テーブルの方にテストケースを書く
– modelのmass asignmentは忘れがちなので注意

BookManagementTest.php

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

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

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

Author.php

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

$ phpunit –filter test_a_new_author_is_automatically_added

tests/Unit/AuthorTest.php

namespace Tests\Unit;

// use PHPUnit\Framework\TestCase;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\Author;

class ExampleTest extends TestCase
{
	use RefreshDatabase;
    public function test_a_dob_is_nullable(){
    	Author::firstOrCreate([
    			'name' => 'John Doe'
    	]);

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

author migration file

Schema::create('authors', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('name');
            $table->timestamp('dob')->nullable();
            $table->timestamps();
        });
public function test_a_new_author_is_automatically_added(){
        $this->withoutExceptionHandling();
        $this->post('/books', [
            'title' => 'Cool Title',
            'author' => 'Victor'
        ]);

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

$ php artisan make:test BookTest –unit

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

Book.php

protected $fillable = ['title', 'author','author_id'];

    public function path(){
    	return '/books/' . $this->id;
    }
public function test_an_author_id_is_recorded()
    {
    	Book::create([
    		'title' => 'Coolest title',
    		'author' => '',
    		'author_id' => 1,
    	]);

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

$ phpunit –filter test_an_author_id_is_recorded
OK (1 test, 1 assertion)

BooksController.php

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

Book.php

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

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

ぐううう

### 全てのmethodをテストするとき
just run phpunit
$ phpunit

### private function

$response = $this->post('/books', [
            'title' => 'Cool Title',
            'author_id' => 1
        ]);
$response = $this->post('/books',$this->data());
private function data() : array{
        return [
            'title' => 'Cool Title',
            'author_id' => 1
        ];
    }

### array_merge

public function test_a_author_is_require(){

        $response = $this->post('/books', array_merge($this->data(), ['author_id'=> '']));

        $response->assertSessionHasErrors('author_id');

    }

OneToOneのテストメソッドでも基本は同じ

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書くだけになのに、時間かかりすぎるな。手が空いている時などはいいかもしれないが、毎回これやってたら開発効率が極端に落ちるように思うので、ケースバイケースだが、積極的には採用しずらい。