Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Advanced Laravel Testing: Do's and Don'ts

Johannes Nagl
February 13, 2020

Advanced Laravel Testing: Do's and Don'ts

Johannes Nagl

February 13, 2020
Tweet

More Decks by Johannes Nagl

Other Decks in Technology

Transcript

  1. 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
  2. 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."
  3. LARAVEL @ SWAT.IO 3 REPOS, 1 BIG ONE: ▸ Tests:

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

    Don't blame me for the wrong wordings. ▸ Integration/Unit? Who cares!
  5. DEV === PROD === TEST USE SAME STACK AS IN

    PRODUCTION, I.E. DON'T TEST WITH SQLITE WHEN YOU'RE RUNNING PGSQL
  6. ▸ Put stuff in non-http environment classes (e.g. repositories) ▸

    Test extensively your repositories (all tests) ▸ Test vaguely your HTTP environment (basic tests)
  7. 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); }
  8. 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); }
  9. $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 );
  10. WHY ASSERT SQL? ▸ Last Minute change introducing a lot

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

    Example? https://github.com/DieSocialisten/Swat.io-API-V2/pull/2164/files
  12. 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)
  13. 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); } } }