This article is not maintained any longer and likely not up to date. DuckDuckGo might help you find an alternative.
Lessons Learnt: Confident Laravel
I took the paid course Confident Laravel when I was on vacation. It is suitable for total testing beginners and a great course you should buy.
Here are my takeaways in the format of question and answer. They are sorted by occurence in the course but you can use whatever you need for your application and testing โ not everything is related to testing:
How to create a basic PHPUnit test (= extend test case)?
class PHPUnit extends TestCase
{
public function testAssertions()
{
$this->assertTrue(true);
}
}
What are the testing definitions?
- What is
$subject
in our tests??- The thing we want to test
- What is
$expected
?- The result we expect!
- What is
$actual
?- Result we actually get
- What is the difference between
$expected
and$actual
?-
$expected
is hardcoded, you just enter a number, a string, a result -
$actual
is a method that returns the actual result
-
What is the order of loading test settings
-
.env
-
.env.testing
-
phpunit.xml
- system environment variables
How to make stuff available to all tests?
Add it to tests/TestCase.php
When and why should I use PHPUnit\Framework\TestCase
directly?
- When Laravel tests features are not needed
- May boost speed when 1000s of tests
How to structure tests in the feature
folder?
Just mimic the app
folder
How to name Dusk (= BDD) tests?
- Give it the name of the feature you test, i.e.
GithubLinksTest
- No need to mimic the folder structure of your app
How to customize Dusk's settings?
- Create
.env.dusk.local
- Make your Dusk specific settings
How to generate tests with subfolders?
-
php artisan make:test Http/Controllers/HomeControllerTest
- No leading
app
- No trailing
.php
How to name factories?
-
ModelnameFactory
- e.g.
UserFactory
(php artisan make:factory UserFactory
)
public function() {
return $this;
}
How to filter php artisan route:list
for entries containing invoice
?
-
php artisan route:list --name invoice
How to test whether the right view gets loaded?
-
$this->assertViewIs('invoice.create')
What is the happy path?
- Testing when the subject is working as expected
How to check in controller if column expired
with timestamp is not set?
-
Model::whereNull('expired')->get()
How to put sth like a coupon in the session cookie?
-
$request->session()->put('name', 'value')
How to name general controller tests?
-
it_does_this_and_that()
- e.g.
it_stores_coupon_and_redirects()
How to check whether data exists in a session?
-
$response->assertSessionHas('key', 'value');
How to get the full error message in Laravel tests?
-
$this->withoutExceptionHandling();
in test method
How many sad paths should you test?
- Enough to feel confident
- There are potentially unlimited sad paths
How to assert that sth is not in a session?
-
$this->assertSessionMissing('key', 'value');
What's the running order in a test?
- Arrange
-
$response = $this->get(route('name'));
-
$response->assertโฆ();
How to mark test incomplete so it gets skipped when running the test suite?
-
$this->markTestIncomplete();
How to test that user is logged in (and not redirected)?
-
$this->assertAuthenticatedAs($user);
- Do not assert on
$response
but on global state$this
How to assert that a view has a variable now_playing
with value 1
?
-
$response->assertViewHas('now_playing', 1);
How to fake a heading or title?
-
$faker->sentence
or$faker->word(3, true)
- Use
true
as 2nd argument in$faker->word
to get words, otherwise you get an array
How to fake an ID?
-
$faker->md5
How to assert the model data you already have in your test?
- Goal: Refresh user record, because it go changed
- Not using:
$this->assertDatabaseHas()
-
$user->refresh();
- Assert properties of model
- Example:
$this->assertEquals(1, $user->last_viewed_vidoe_id)
How to add $faker
to your test?
- Add
use WithFaker;
trait to your test class
How to create a password hash?
-
Hash::make('STRING')
How to assert two hashes?
- NOT:
$this->assertEquals(Hash::make($password, $user->password);
- BUT:
$this->assertTrue(Hash::check($password, $user->password);
What is the difference between asserting Hash::check()
and hardcoding a password value?
-
Hash:check
: tie test to the implementation of the HASH function - Hardcoding: Setting a sample value is more flexible but also more error prone
How to check if session contains error 'name'?
-
$response->assertSessionHasErrors('name');
Why do form requests not auto redirect back and how to work around that?
- They send referrer URL
- These are fresh with every request => no history available
- Instead: Set the referrer URL manually in the setup
-
$this->from(route('user.edit'))
How to avoid writing dozens of assertions in form validations?
- Use Form Requests
- Assert that Form Request gets used by the controller
- Use Jason McCreary's Laravel Test Asserts for that
<?php
// In test method
$this->assertActionUsesFormRequest(
UsersController::class, // Controller
'update', // Method
UserUpdateRequest::class); // Form Request
);
How to find incomplete tests?
-
phpunit -v
How to directly assert non-auth cannot access?
-
$this->assertForbidden();
How to manually set response 'forbidden' on controller?
-
abort(403)
How to get Product::STARTER
?
- This is a constant in the product model
What is a mock?
- Stand-in replacement for real object
- Emulates
echo
behaviour
// What is the simplest mock you can create?
class ExampleTest extends \PHPUnit\Framework\TestCase
{
public function testMocking()
{
$mock = \Mockery::mock();
$mock->shouldReceive('foo') // method name
->with('bar') // arg
->andReturn('baz'); // arg
$this->assertEquals('baz', $mock->foo('bar'));
}
}
What are mocks great for?
- Testing integratin with 3rd party libraries without calling them
- Control testing in complex situations, because you break down problems in small chunks
When will mocks fail?
- If not set
- If it does not have an expectation
What is a spy?
- It secretly captures behavior
- Returns null if not setup at all
- More forgiving than mock
What's the easiest spy you can write?
public function testSpying()
{
$spy = Mockery::spy();
$this->assertNull($spy->quix());
// assert behavior
$spy->shouldHaveReceived('quix')->once();
}
What are test doubles?
- Replace production object for testing purposes
Why are facades easy to test in Laravel?
- Every facade has mocks & spies
How to return nothing (do nothing after a request)?
return response(null, 204)
How to mock Laravel's Log
facade?
$mock = \Mockery::mock();
$mock->shouldReceive('info') // method name
->once()
->with('video.watched', [$video->id]);
Log::swap($mock);
What is the difficulty with shared facades?
- Facades like
Event
orLog
get used all over Laravel - If you swap them in your test, they get swapped everywhere
- This may cause trouble
- You can use
Event::fake()
instead ofEvent::swap()
- Example: To check if event was fired:
$event->assertDispatched();
What to do instead of mocks?
- When you can't fake or don't want to create dozens of mock classes
- Wrape it and decouple from tests
What happens in this mock?
- The real
Log::
class gets swapped by the mock - The mock tells what should happen as a result
- As there is an expectation (= log vars), you need no extra assertion
Why do I need the Log::
facade at all?
- It's the original code
- It would go untested otherwise
Why do you use ->once()
in a mock?
- Otherwise the test will pass if the double in the real code is not called at all
- i.e. only with
->once()
, you can make sure that the facade got used in the real code
What is a shortcut for $mock->shouldReceive('info')->once()
?
-
$mock->expects('info');
// method name
How to create a setup method that runs for all tests in a file?
protected function setUp(): void
{
parent::setUp();
$this->subject = new UserUpdateRequest();
}
Use it with $this->subject
in the tests
How to run a validation in an unit test?
$subject = new UserUpdateRequest();
// will call the file App\Http\Requests\UserUpdateRequest;
How to create a coverage report
- You need xdebug
- You can use a docker image with xdebug
- Run
phpunit --coverage-html report
- Spin up a PHP server to view reports with
php -S 0:8080 -t report/