Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Advanced Laravel Testing: Do's and Don'ts

Cdc67badcd6322c88bd92d4dbb391ff9?s=47 Johannes Nagl
February 13, 2020

Advanced Laravel Testing: Do's and Don'ts

Cdc67badcd6322c88bd92d4dbb391ff9?s=128

Johannes Nagl

February 13, 2020
Tweet

More Decks by Johannes Nagl

Other Decks in Technology

Transcript

  1. ADVANCED TESTING DOS' AND DON'TS SWAT.IO, @jo ife CTO, #proudDadOfAGirl,

    Life Long Learner Tester
  2. SWAT.IO SOCIAL MEDIA MANAGEMENT FOR TEAMS ▸ 7 Developers, 8

    servers, 2.5 TB of database ▸ PHP7.3, PostgreSQL, ElasticSearch, Laravel, CakePHP, Queues, Queues, Queues ▸ Lots of high volume customers ▸ 6 Mio API outgoing requests/day ▸ 0.7 Mio incoming webhooks/day ▸ Only constant: Change
  3. SKILLS "We <3 to take full advantage of our abilities

    and talents, as well as further developing them together." ACTION "We don't default to blindly following the rules. We encourage everyone to give feedback and show initiative at any time."
  4. LARAVEL @ SWAT.IO 3 REPOS, 1 BIG ONE: ▸ Tests:

    7295, Assertions: 260948, Skipped: 5
  5. DISCLAIMER ▸ I'm a hands-on guy, not a scientist. ▸

    Don't blame me for the wrong wordings. ▸ Integration/Unit? Who cares!
  6. I'M HERE FOR A show-o . AMA

  7. I'M HERE FOR A discu ion.

  8. I MIGHT BE HERE FOR A FLAMEWAR.

  9. 1. NO-

  10. DEV === PROD === TEST USE SAME STACK AS IN

    PRODUCTION, I.E. DON'T TEST WITH SQLITE WHEN YOU'RE RUNNING PGSQL
  11. 2. NO-

  12. USE A DEDICATED TEST DB DON'T REUSE YOUR LOCAL DEV

    DB
  13. 3. NO-

  14. SETUP YOUR TEST RUNNER INSIDE PHPSTORM @group Tests that should

    run together
  15. 4. NO-

  16. MOCK HTTP / EXTERNAL APIS

  17. 5. NO-

  18. USE CARBON::SETTESTNOW() - ALL THE time!

  19. None
  20. 6. ! - !

  21. USE DATAPROVIDERS AS MUCH AS POSSIBLE!

  22. 7. ! - !

  23. USE FACTORIES TO DEFINE YOUR FIXTURES! FOR EVERY SINGLE TEST

  24. ▸ we learned it the hard way. ▸ fixture_data.php ▸

    fakeFactory FTW!
  25. 8. ! -

  26. DO NOT TEST YOUR HTTP IMPLEMENTATION

  27. IT'S SLOW.

  28. ▸ Put stuff in non-http environment classes (e.g. repositories) ▸

    Test extensively your repositories (all tests) ▸ Test vaguely your HTTP environment (basic tests)
  29. YOUR FUTURE YOU WILL BE THANKFUL.

  30. YOUR FUTURE YOU WILL BE VERY ANGRY.

  31. 9. ! -

  32. MY LEGACY AKA: WTF AKA: OMG!

  33. ASSERT YOUR SQL

  34. protected function setupSqlCountListenForQueries(): void { DB::listen(function (QueryExecuted $event) { $this->sqlQueryEvents[]

    = $event; }); }
  35. protected function assertSqlCount(int $expectedCount, string $msg = ''): void {

    $numSqlQueries = \count($this->sqlQueryEvents); if ($expectedCount === $numSqlQueries) { $this->sqlCounterReset(); return; } $msg .= sprintf("Expected number of SQL statements of %d does not match the actual value of %d\nQueries:\n\n%s\n", $expectedCount, $numSqlQueries, implode("\n", array_map( function (QueryExecuted $query) { return sprintf('[%s] %s', $query->connectionName, $query->sql ); }, $this->sqlQueryEvents ) ) ); $this->assertSame($expectedCount, $numSqlQueries, $msg); }
  36. $this->assertSqlCount(0); //

  37. $this->dbHeavyMethod(); $this->assertSqlCount(67); // $this->sqlCounterReset();

  38. $this->initCodeOfYourTestCase(); $this->sqlCounterReset(); $this->realTestStuff(); $this->assertSqlCount(5); //

  39. 9++. ! -

  40. @LIFE_AND_DEV LEGACY AKA: WTF++ AKA: OMG++!

  41. protected function assertSqlQueries(string $expectedQueries, string $msg = ''): void {

    $expectedQueries = trim($expectedQueries); $actualQueries = trim( implode("\n", array_map( function (QueryExecuted $query): string { // Replace any numeric literals with "fake" bind // placeholders. The framework recently optimized // whereIn queries to contain all-only integer // literals directly, which means it includes // IDs which may change during multiple test // runs, which we now manually need to normalize return preg_replace( [ // Covers integers in `WHERE IN ()` '/\d+(,|\))/', // Covers simple `WHERE x =` '/= \d+/', ], [ '?$1', '= ?', ], $query->sql) . ';'; }, $this->sqlQueryEvents ) ) ); $this->sqlCounterReset(); if (!$msg) { $msg = 'SQL queries mismatch'; } $this->assertSame($expectedQueries, $actualQueries, $msg); }
  42. $this->assertSqlQueries(<<<'SQL' insert into "cron_logs" ("action", "class_name", "max_runtime", "params", "started", "trace_id")

    values (?, ?, ?, ?, ?, ?) returning "id"; update "cron_logs" set "finished" = ? where "id" = ?; SQL );
  43. WHY?

  44. WHY ASSERT SQL? ▸ Last Minute change introducing a lot

    of new sql statements? ▸ N+1 Problem: Gone!
  45. WHY NOT ASSERT SQL? ▸ Refactoring is a PITA ▸

    Example? https://github.com/DieSocialisten/Swat.io-API-V2/pull/2164/files
  46. GIST LINK INCOMING! https://gist.github.com/johannesnagl/ 3bbfae042d85d8949f1cca2d1ce90539

  47. Bonus!

  48. PHPSTAN FIRST DEVELOPMENT STATIC ANALYSIS FTW discover bugs in your

    code without before ru ing it! https://github.com/phpstan/phpstan/ https://github.com/nunomaduro/larastan Different levels to jump in! (We're at 6)
  49. ONE MOAR THING!

  50. ASSERTMODELATTRIBUTESSAME protected function assertModelAttributesSame(Model $model, array $expected): void { $className

    = get_class($model); foreach ($expected as $attribute => $value) { $message = "Assert for model $className failed on attribute $attribute"; if ($value instanceof CarbonImmutable) { static::assertCarbonEqual($value, $model->$attribute, $message); } else { $this->assertSame($value, $model->$attribute, $message); } } }
  51. QUESTIONS? Answers!

  52. QUESTIONS? Answers! OF COURSE, WE'RE LOOKING FOR NEW team MEMBERS!