https://www.youtube.com/playlist?list=PLe30vg_FG4OTsFRc1eWppZfYwZdMlLuhE
- PHPUnit Testing
- by Bitfumes Webnologies
Notes & disclaimer:
- The purpose of the note 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 search.
- Allowing a particular video to be found and re-watched.
- During the notes two versions of PHPUnit are used:
- 7.5.9 is installed with Laravel
- 8.0.9 was previously installed globally, although this isn't the recommended way, it does allow PHPUnit to be run from any folder.
- Code snippets are often used to highlight the code changed, any code prior or post the code snipped is generally unchanged from previous notes, or to highlight only the output of interest. To signify a snippet of a larger code block, dots are normally used e.g.
\\ ...
echo "Hello";
\\ ...
Introduction to testing.
What is unit testing.
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>
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:
/**
* @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']
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' => '[email protected]',
'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)
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)
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);
}
}
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, [email protected], 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' => '[email protected]',
'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)
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)
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)
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)