Skip to content

Instantly share code, notes, and snippets.

@Pen-y-Fan
Created March 28, 2020 10:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Pen-y-Fan/89d81333ed3c3d6a0fd7212c9b78e0fd to your computer and use it in GitHub Desktop.
Save Pen-y-Fan/89d81333ed3c3d6a0fd7212c9b78e0fd to your computer and use it in GitHub Desktop.
PHPUnit Testing Based on YouTube Series by Bitfumes

PHPUnit Testing

https://www.youtube.com/playlist?list=PLe30vg_FG4OTsFRc1eWppZfYwZdMlLuhE

Notes & disclaimer

  • The purpose of these notes are to follow the above tutorial, making detailed notes of each step.
  • They are not verbatim of the original video.
  • Although the notes are detailed, it is possible they may not make sense out of context.
  • The notes are not intended as a replacement the video series
    • Notes are more of a companion
    • They allow an easy reference, making it easy to search in one file to find content.
    • Allowing a particular video to be found and re-watched.

Lesson 1 9:45 Why we need Testing of Code | Code Testing #1

Introduction to testing.

Lesson 2 5:33 What is Unit Testing | Code Testing #2

What is unit testing.

Lesson 3 9:37 Getting Started with PHPUnit | Code Testing #3

How to install PHP Unit.

composer require phpunit/phpunit ^7 --dev
./composer.json has been created
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 29 installs, 0 updates, 0 removals
  - Installing sebastian/version (2.0.1): Loading from cache
  - Installing sebastian/resource-operations (1.0.0): Downloading (100%)
  - Installing sebastian/recursion-context (3.0.0): Loading from cache
  - Installing sebastian/object-reflector (1.1.1): Loading from cache
  - Installing sebastian/object-enumerator (3.0.3): Loading from cache
  - Installing sebastian/global-state (2.0.0): Loading from cache
  - Installing sebastian/exporter (3.1.0): Loading from cache
  - Installing sebastian/environment (3.1.0): Downloading (100%)
  - Installing sebastian/diff (3.0.2): Loading from cache
  - Installing sebastian/comparator (2.1.3): Downloading (100%)
  - Installing doctrine/instantiator (1.2.0): Loading from cache
  - Installing phpunit/php-text-template (1.2.1): Loading from cache
  - Installing phpunit/phpunit-mock-objects (6.1.2): Downloading (100%)
  - Installing phpunit/php-timer (2.1.1): Loading from cache
  - Installing phpunit/php-file-iterator (1.4.5): Loading from cache
  - Installing theseer/tokenizer (1.1.2): Loading from cache
  - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache
  - Installing phpunit/php-token-stream (3.0.1): Loading from cache
  - Installing phpunit/php-code-coverage (6.0.5): Downloading (100%)
  - Installing symfony/polyfill-ctype (v1.11.0): Loading from cache
  - Installing webmozart/assert (1.4.0): Loading from cache
  - Installing phpdocumentor/reflection-common (1.0.1): Loading from cache
  - Installing phpdocumentor/type-resolver (0.4.0): Loading from cache
  - Installing phpdocumentor/reflection-docblock (4.3.0): Loading from cache
  - Installing phpspec/prophecy (1.8.0): Loading from cache
  - Installing phar-io/version (1.0.1): Downloading (100%)
  - Installing phar-io/manifest (1.0.1): Downloading (100%)
  - Installing myclabs/deep-copy (1.9.1): Loading from cache
  - Installing phpunit/phpunit (7.0.0): Downloading (100%)
sebastian/global-state suggests installing ext-uopz (*)
phpunit/phpunit-mock-objects suggests installing ext-soap (*)
phpunit/phpunit suggests installing phpunit/php-invoker (^2.0)
Package phpunit/phpunit-mock-objects is abandoned, you should avoid using it. No replacement was suggested.
Writing lock file
Generating autoload files

phpunit.xml file create in the root

<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         verbose="true"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
</phpunit>

Lesson 4 10:58 Write your First Ever Test | Code Testing #4

Install laravel 5.6

composer create-project --prefer-dist laravel/laravel testing ^5.6

Open testingLaravel\tests\Feature\ExampleTest.php

Create a route for route about:

Lesson 5 9:34 Start Laravel Testing | Feature Test | Code Testing #5

/**
 * @test
 *
 * @return void
  */
public function aboutRoutReturnAbout()
{
    $response = $this->get('/about');
    // dd($response);
    $response->assertOK()->assertSee('About');
}

We have a failing test.

Open web.php

  • Add a route for about which returns About
Route::get('/about', function () {
    return 'About';
});

The test now passes:

PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 1.01 seconds, Memory: 14.00 MB

OK (1 test, 2 assertions)

For documentation see https://laravel.com/docs/5.8/http-tests#available-assertions

The home page can be tested assertSee('Laravel'), or assertSeeInOrder['Laravel', 'Documentation']

Lesson 6 9:53 What is Database Transaction in Laravel Testing | Code Testing #6

To create a unit test run the artisan command make test.

php artisan make:test UserModelTest --unit
Test created successfully.

Open UserModelTest.php:

/**
  * @test
  *
  * @return void
  */
public function userHasFullNameAttribute()
{
    // create user
    $user = User::create([
        'firstname' => 'Fred',
        'lastname'  => 'Bloggs',
        'email'     => 'Fred@bloggs.test',
        'password'  => 'secret',
    ]);

    // assert user has full name
    $this->assertEquals('Fred Bloggs', $user->fullName);
}

Update the user_table, to add firstname and lastname:

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->increments('id');
        $table->string('firstname');
        $table->string('lastname');
        $table->string('email')->unique();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

Update the User.php model:

  • Assignable includes firstname and lastname
  • create a method to getFullNameAttribute
// ...
protected $fillable = [
    'firstname',
    'lastname',
    'email',
    'password',
];
// ...

public function getFullNameAttribute()
{
    return "$this->firstname $this->lastname";
}

Update the phpunit.xml to run from memory:

  • set DB_CONNECTION to sqlite
  • set DB_DATABASE to :memory:
<php>
    <env name="APP_ENV" value="testing"/>
    <env name="DB_CONNECTION" value="sqlite"/>
    <env name="DB_DATABASE" value=":memory:"/>
    <env name="CACHE_DRIVER" value="array"/>
    <env name="SESSION_DRIVER" value="array"/>
    <env name="QUEUE_DRIVER" value="sync"/>
</php>

All test pass green:

PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

....                                                                4 / 4 (100%)

Time: 6.3 seconds, Memory: 24.00 MB

OK (4 tests, 4 assertions)

Lesson 7 5:02 Why use Database Migration | Laravel Testing #7

Instead of use RefreshDatabase; try use DatabaseMigrations. There is no need to run the setUp to migrate.

All tests pass.

PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

....                                                                4 / 4 (100%)

Time: 1.92 seconds, Memory: 24.00 MB

OK (4 tests, 4 assertions)

Lesson 8 8:10 Test with Model Factory | Laravel Testing #8

This lesson we will make a new test and a factory.

php artisan make:test BeverageTest --unit
#Test created successfully.

Open BeverageTest.php

  • Add use DatabaseMigrations;
  • Import class
  • create a model called beverageHasName()

make the model and factory for Beverage

php artisan make:model Beverage -mf

In the beverage_table:

  • configure the fields:
public function up()
{
    Schema::create('beverages', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name'); // Add
        $table->string('type'); // Add
        $table->timestamps();
    });

Open BeverageFactory.php

$factory->define(App\Beverage::class, function (Faker $faker) {
    return [
        'name' => $faker->word,
        'type' => $faker->beverageName(),
    ];
});

For beverage there is another package

composer require jzonta/faker-restaurant

In BeverageTest.php

  • Create a setup method to create a Beverage using the factory
  • Create two methods to check the beverage name and beverage type
<?php

namespace Tests\Unit;

use App\Beverage;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class BeverageTest extends TestCase
{
    use DatabaseMigrations;

    private $beverage;

    public function setUp(): void
    {
        parent::setup();

        // create beverage
        $this->beverage = factory(Beverage::class)->make();
    }
    /**
     * @test
     * @return void
     */
    public function beverageHasName()
    {
        // assert
        $this->assertNotEmpty($this->beverage->name);
    }

    /**
     * @test
     * @return void
     */
    public function beverageHasType()
    {
        // assert
        $this->assertNotEmpty($this->beverage->type);
    }
}

Lesson 9 10:39 Test with Logged in User and Exception Testing | Laravel Testing #9

When an exception is triggered and thrown we need to expect assert that exception.

Open BeverageTest.php:

  • create a method a user can not buy alcoholic beverage
/**
 * a user can not buy an alcoholic beverage
 *
 * @test
 * @return void
    */
public function UserCanNotBuyAlcoholicBeverage()
{
    // a alchoolic beverage
    $this->beverage = factory(Beverage::class)->make([
        'type' => 'Alcoholic',
    ]);

    // minor user
    $user = factory(User::class)->make([
        'age' => '17',
    ]);

    // logged in
    $this->actingAs($user);

    // buy berverage
    $this->beverage->buy();

 // except exception
    $this->expectException(MinorCannotBuyAlchoholicBeverageException::class);
}

Add buy method to the Beverage class

use App\Exceptions\MinorCannotBuyAlchoholicBeverageException;
// ...

/**
 * Allow a user to buy a beverage
 *
 * @return boolean true|exception
    */
public function buy()
{
    if (auth()->user()->isMinor()) {
        throw new MinorCannotBuyAlchoholicBeverageException;
    }
    // logic to buy a beverage
    return true;
}

Add the isMinor method to the User class

public function isMinor()
{
    return $this->age < 18 ? true : false;
}

Test fails unable to find MinorCannotBuyAlchoholicBeverageException

Create the exception class

php artisan make:exception MinorCannotBuyAlchoholicBeverageException
# Exception created successfully.

Import the class to BeverageTest.php and Beverage.php

Runt the test and it still fails.

There was 1 error:

1) Tests\Unit\BeverageTest::UserCanNotBuyAlcoholicBeverage
App\Exceptions\MinorCannotBuyAlchoholicBeverageException:

Re-order the test so the expectException is before the buy() call.

use App\User;
use App\Exceptions\MinorCannotBuyAlchoholicBeverageException;
// **
/**
 * a user can not buy an alcoholic beverage
 *
 * @test
 * @return void
    */
public function UserCanNotBuyAlcoholicBeverage()
{
    // a alchoolic beverage
    $this->beverage = factory(Beverage::class)->make([
        'type' => 'Alcoholic',
    ]);

    // minor user
    $user = factory(User::class)->make([
        'age' => '17',
    ]);

    // logged in
    $this->actingAs($user);

    // except exception
    $this->expectException(MinorCannotBuyAlchoholicBeverageException::class);

    // buy berverage
    $this->beverage->buy();

}
phpunit --filter UserCanNotBuyAlcoholicBeverage
PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

.                                                                   1 / 1 (100%)

Time: 1.47 seconds, Memory: 26.00 MB

OK (1 test, 1 assertion)

Run all tests

phpunit
PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

.......                                                             7 / 7 (100%)

Time: 3.38 seconds, Memory: 28.00 MB

OK (7 tests, 7 assertions)

Normally the User class does not have an age attribute, so write a test to confirm it has one.

In testingLaravel\tests\Unit\ExampleTest.php

use App\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
// ...

class ExampleTest extends TestCase
{
    use DatabaseMigrations;
// ...

/**
 * @test
 */
public function userHasAgeAttribute()
{
    $user = factory(User::class)->make();

    $this->assertNotNull($user->age);
}

The test fails:

...
There was 1 failure:

1) Tests\Unit\ExampleTest::userHasAgeAttribute
Failed asserting that null is not null.
...

Open create_user_table and add the age field

// ...
$table->string('lastname');
$table->integer('age'); // Added
$table->string('email')->unique();
// ...

Open the UserFactory.php, add the age faker.

// ..
$factory->define(App\User::class, function (Faker $faker) {
    return [
        'firstname'      => $faker->name,
        'lastname'       => $faker->name,
        'age'            => $faker->numberBetween(10, 50), // Add
        'email'          => $faker->unique()->safeEmail,
        'password'       => '$2y$10$TKh8H1.PfQx37YgCzwiKb.KjNyWgaHb9cbcoQgdIVFlYg7B77UdFm', // secret
        'remember_token' => str_random(10),
    ];
});
// ..

In User.php add the field to the fillable array

protected $fillable = [
    'firstname',
    'lastname',
    'age', // Add
    'email',
    'password',
];

Run the test and it now passes.

phpunit --filter userHasAgeAttribute
PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

.                                                                   1 / 1 (100%)

Time: 1.4 seconds, Memory: 24.00 MB

OK (1 test, 1 assertion)

Run all tests, the UserModelTest fails as age isn't provided

1) Tests\Unit\UserModelTest::userHasFullNameAttribute
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: users.age (SQL: insert into "users" ("firstname", "lastname", "email", "password", "updated_at", "created_at") values (Fred, Bloggs, Fred@bloggs.test, secret, 2019-04-26 10:51:06, 2019-04-26 10:51:06))

Open testingLaravel\tests\Unit\UserModelTest.php

public function userHasFullNameAttribute()
    {
        // create user
        $user = User::create([
            'firstname' => 'Fred',
            'lastname'  => 'Bloggs',
            'age'       =>  21,
            'email'     => 'Fred@bloggs.test',
            'password'  => 'secret',
        ]);

Run the test and it passes. Run all tests and they all pass.

phpunit
PHPUnit 8.0.6 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

........                                                            8 / 8 (100%)

Time: 4.32 seconds, Memory: 28.00 MB

OK (8 tests, 8 assertions)

Lesson 10 10:03 Visit to see all Beverages| TDD Approach | Laravel Testing #10

We will write feature tests, still focusing on Beverages.

php artisan make:test BeverageTest

Open testingLaravel\tests\Feature\BeverageTest.php

  • use DatabaseMigrations and import the class.
  • create a method a user can visit a beverage page and see beverages
  • user will go to a url
    • $response = $this->get('/beverage')
  • assert status
    • $response->assertOK;
<?php

namespace Tests\Feature;

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

class BeverageTest extends TestCase
{
    use DatabaseMigrations;

    /**
     * @test
     *
     * @return void
     */
    public function aUserCanVisitABeveragePageAndSeeBeverages()
    {
        // user will go to a url
        $response = $this->get('/beverage');
        // assert status
        $response->assertOK();
    }
}

Run the test and it fails.

...
Response status code [404] does not match expected 200 status code.
...

Now we can create the Route, open web.php

// ...
Route::resource('/beverage', 'BeverageController');

Run the test and it passes! The BeverageController.php was created earlier and has all verbs. If the controller didn't exist the test would return code 500 (server error). Temporarily rename BeverageController.php > BeverageController2.php, then rerun the test. It fails:

...
Response status code [500] does not match expected 200 status code.
...

To see more details open \app\Exceptions\Handler.php

  • add a statement to throw the exception when testing.
public function report(Exception $exception)
{
    if (app()->environment() == 'testing') {
        throw $exception;
    }

    parent::report($exception);
}

We can now see the exception, there is no BeverageController.php file:

...
ErrorException: include(C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\vendor\composer/../../app/Http/Controllers/BeverageController.php): failed to open stream: No such file or directory
...

Rename the BeverageController back, or if there wasn't one, create it:

php artisan make:controller BeverageController -m Beverage

Run the test and it will now pass! The BeverageController has the index method, which returns nothing, so an empty page is successfully returned.

Open the BeverageController.php

In the index method.

// ..
public function index()
{
    return view('beverage.index');
}
// ..

Run the test and there is a new error:

InvalidArgumentException: View [beverage.index] not found.

Create a view for beverage:

  • create the beverage directory in views
  • create an empty file called index.blade.php

Re-run the test and it now passes.

Now there is a passing test the code can be refactored and written, with the test being re-run at each stage.

Open resources\views\beverage\index.blade.php.

@extends('layouts.app');

@section('content')


@endsection
ErrorException: View [layouts.app] not found.

Create a new directory called layouts, create a new file app.blade.php in the directory.

<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <!-- CSRF Token -->
    <meta name="csrf-token" content="{{ csrf_token() }}">

    <title>{{ config('app.name', 'Laravel') }}</title>

    <!-- Scripts -->
    <script src="{{ asset('js/app.js') }}" defer></script>

    <!-- Fonts -->
    <link rel="dns-prefetch" href="//fonts.gstatic.com">
    <link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">

    <!-- Styles -->
    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
    <div id="app">
        <main class="py-4">
            <div class="container">
                @yield('content')
            </div>
        </main>
    </div>
</body>
</html>

Re-run test, now passes green:

OK (1 test, 1 assertion)

Update the test to add an assertion to see a beverage name.

  • Note: the ->create helper function will persist the factory data to the in memory sqlite database.
public function aUserCanVisitABeveragePageAndSeeBeverages()
{
    $beverage = factory(Beverage::class)->create();
    // user will go to a url
    $response = $this->get('/beverage');
    // assert statusOK
    $response->assertOK();

    // assert see a beverage name
    $response->assertSee($beverage->name);
}

We need to see all the beverages. In the BeverageController.php

  • add $beverages = Beverage::all();
  • return all the $beverages to the view using compact.
public function index()
{
    $beverages = Beverage::all();
    return view('beverage.index', compact('beverages'));
}
There was 1 failure:

1) Tests\Feature\BeverageTest::aUserCanVisitABeveragePageAndSeeBeverages
Failed asserting that ';\r\n
...</html>\n
' contains "quos".

In index.blade.php

@section('content')
    <h1>Test</h1>
    @foreach ($beverages as $beverage)
        <h2>{{ $beverage->name }}</h2>
    @endforeach

@endsection

Re-run he test and it passes:

...
OK (1 test, 2 assertions)

Lesson 11 6:16 Visit a single Beverage | Laravel Testing #11

This episode will be for a single Beverage (show).

In Feature/BeverageTest.php

  • create a new test called a user can visit a single beverage page
// ...

/**
 * @test
 */
public function aUserCanVisitASingleBeveragePage():void
{
    $beverage = factory(Beverage::class)->create();
    $response = $this->get('/beverage/'.$beverage->id);
    $response->assertOK();
    $response->assertSee($beverage->name);
}
// ...

Run the test:

Failed asserting that '' contains "et".

The show method on the BeverageController.php already exists, so is returning an empty view.

Open the controller and in the show method:

  • return the view beverage.show
public function show(Beverage $beverage)
{
    return view('beverage.show', compact('beverage'));
}

Test now fails:

InvalidArgumentException: View [beverage.show] not found.

Duplicate beverage/index.blade.php call teh duplicate beverage.show.php, open beverage

@extends('layouts.app');

@section('content')
    <h1>Show Test</h1>

    <h2>{{ $beverage->name }}</h2>

@endsection

Run the test and it passes:

OK (1 test, 2 assertions)

The tests have duplicate setup, instead we can use the setUp function.

In the BeverageTest.php

  • create new public method called setUp
  • Move the duplicate code for creating a factory
  • create a private property called $beverage
  • convert all the references to $beverage to $this->beverage
<?php

namespace Tests\Feature;

use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use App\Beverage;

class BeverageTest extends TestCase
{
    use DatabaseMigrations;

    private $beverage;

    public function setUp():void
    {
        parent::setUp();
        $this->beverage = factory(Beverage::class)->create();
    }
    /**
     * @test
     */
    public function aUserCanVisitABeveragePageAndSeeBeverages():void
    {

        // user will go to a url /beverage/{id}
        $response = $this->get('/beverage');
        // assert status
        $response->assertOK();

        // assert see a beverage name
        $response->assertSee($this->beverage->name);
    }

    /**
     * @test
     *
     * @return void
     */
    public function aUserCanVisitASingleBeveragePage():void
    {
        $response = $this->get('/beverage/'.$this->beverage->id);
        $response->assertOK();
        $response->assertSee($this->beverage->name);
    }
}

Rerun both tests and they pass.

OK (2 tests, 4 assertions)

Run all tests and they pass:

PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

..........                                                        10 / 10 (100%)

Time: 5.5 seconds, Memory: 22.00 MB

OK (10 tests, 12 assertions)

Lesson 12 11:05 Authenticated User Buy a Beverage | Test a Post Route | Laravel Testing #12

This episode will test if a logged in user can buy a berverage..

Still in BerverageTest.php

  • create a new test public method called a logged in user can buy beverage
  • Add the details for the test
public function aLoggedInUserCanBuyBeverage() :void
{
    // logged in user
    $user = factory(User::class)->create();
    $this->actingAs($user);

    $data = [
        'quantity'    => 1,
        'beverage_id' => $this->beverage->id,
        'user_id'     => $user->id,
        'price'       => 200,
    ];

    // post a data for buying
    $response = $this->post('/beverage/buy', $data);

    // assert in database
    $this->assertDatabaseHas($data);

    // status
    $response->assertStatus(201);
}

The test fails for:

Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException:
...
C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\tests\Feature\BeverageTest.php:66
...

This is the post route to /beverage/buy, this route does not exist.

In web.php router, create a new route to a new controller:

Route::resource('/beverage', 'BeverageController');
Route::post('/beverage/buy', 'PurchaseController@buy'); // Add

tests now confirms the controller doesn't exist:

...
ReflectionException: Class App\Http\Controllers\PurchaseController does not exist
...

Make the controller:

php artisan make:controller PurchaseController

Re-run the test:

...
BadMethodCallException: Method App\Http\Controllers\PurchaseController::buy does not exist.
...

Open the PurchaseController.php

  • add a buy method
// ...
class PurchaseController extends Controller
{
    public function buy()
    {
        // code
    }
}

Re-run the test:

...
ArgumentCountError: Too few arguments to function Illuminate\Foundation\Testing\TestCase::assertDatabaseHas(), 1 passed in C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\tests\Feature\BeverageTest.php on line 69 and at least 2 expected
...

This means there is an error with the test, $this->assertDatabaseHas($data); needs two arguments, the first is the table, then the data. Update the test to read:

$this->assertDatabaseHas('purchases', $data);

Re-run the test:

Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1 no such table: purchases (SQL: select count(*) as aggregate from "purchases" where ("qty" = 1 and "beverage_id" = 1 and "user_id" = 1 and "price" = 200)

As expected, there is no purchases table.

  • Create a model for Purchase, with a migration and factory.
php artisan make:model Purchase -mf

Open the create_purchase_table:

  • In the up method add the required tables
public function up()
{
    Schema::create('purchases', function (Blueprint $table) {
        $table->increments('id');
        $table->unsignedBigInteger('beverage_id');
        $table->unsignedBigInteger('user_id');
        $table->unsignedBigInteger('price');
        $table->unsignedBigInteger('quantity');
        $table->timestamps();
    });

re-run the test:

...
Failed asserting that a row in the table [purchases] matches the attributes {
    "quantity": 1,
    "beverage_id": 1,
    "user_id": 1,
    "price": 200
}.

The table is empty.
...

Open the PurchaseFactory.php

  • return the faker data.
$factory->define(App\Purchase::class, function (Faker $faker) {
    return [
        'beverage_id' => $faker->randomDigitNotNull,
        'user_id'     => $faker->randomDigitNotNull,
        'price'       => $faker->numberBetween(100, 200),
        'quantity'    => $faker->randomDigitNotNull,
    ];
});

Re-run the test.

The table is empty.

Now the faker factory is ready, but the data isn't created when the purchase is made. Open the PurchaseController.php, in the buy method:

  • Add the request as input parameters
  • Add Purchase::create($request->all());
  • Remember to import the classes
  • then return to the originator with 201 status code.
use Illuminate\Http\Request; // Add
use App\Purchase; // Add

class PurchaseController extends Controller
{
    public function buy(Request $request)
    {
        Purchase::create($request->all());
        return response(null, 201);
    }
}

Re-run the test.

Illuminate\Database\Eloquent\MassAssignmentException: Add [quantity] to fillable property to allow mass assignment on [App\Purchase].

The guarded or fillable properties have not be set. Open Purchase model.

  • Add a protected guarded property with an array of id, all other fields can be added.
class Purchase extends Model
{
    protected $guarded = ['id'];
}

Re-run the test and it passes.

OK (1 test, 2 assertions)

However the middleware for auth needs to be added to the PurchaseController.php

  • Add a constructor
    • With middleware of auth.
// ...
class PurchaseController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }
// ...

As a negative test in the BeverageTest comment out the actingAs user line.

Re-run the test and it fails.

Illuminate\Auth\AuthenticationException: Unauthenticated.

Uncomment the actingAs line, re-run the test and it passes.

Some of the tests can be refactored. The code to create a logged in user is used is several places:

$user = factory(User::class)->create();
$this->actingAs($user);

Open TestCase.php

  • create a public method authenticatedUser and add the code
  • import the User class
// ...
use App\User;
// ...
protected $user;
public function authenticatedUser()
{
    $this->user = factory(User::class)->create();
    $this->actingAs($this->user);
}

In the BeverageTest.php change all occurrences of the code to $this->authenticatedUser();

public function aLoggedInUserCanBuyBeverage() :void
{
    // logged in user
    $this->authenticatedUser(); // Update

    $data = [
        'quantity'    => 1,
        'beverage_id' => $this->beverage->id,
        'user_id'     => $this->user->id, // Update
        'price'       => 200,
    ];

    // post a data for buying
    $response = $this->post('/beverage/buy', $data);

    // assert in database
    $this->assertDatabaseHas('purchases', $data);

    // status
    $response->assertStatus(201);
}

Run all tests and they pass.

vendor\bin\phpunit
PHPUnit 7.5.9 by Sebastian Bergmann and contributors.

Runtime:       PHP 7.3.4 with Xdebug 2.7.1
Configuration: C:\laragon\www\YouTube\Code-Testing-Bitfumes\testingLaravel\phpunit.xml

...........                                                       11 / 11 (100%)

Time: 6.4 seconds, Memory: 22.00 MB

OK (11 tests, 14 assertions)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment