DEV Community

Martin Betz
Martin Betz

Posted on • Originally published at martinbetz.eu on

Lessons Learnt: Confident Laravel

I took the course Confident Laravel when I was on vacation. It is suitable for total testing beginners.

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

  1. .env
  2. .env.testing
  3. phpunit.xml
  4. 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?

<?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 or Log 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 of Event::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/

Top comments (1)

Collapse
 
channaveer profile image
Channaveer Hakari

Awesome sum up. Felt refreshed with the concepts. Thank you for this amazing writeup.