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

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. ADVANCED TESTING
    DOS' AND DON'TS
    SWAT.IO, @jo ife
    CTO, #proudDadOfAGirl,
    Life Long Learner Tester

    View Slide

  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

    View Slide

  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."

    View Slide

  4. LARAVEL @ SWAT.IO
    3 REPOS,
    1 BIG ONE:
    ▸ Tests: 7295, Assertions: 260948, Skipped: 5

    View Slide

  5. DISCLAIMER
    ▸ I'm a hands-on guy, not a scientist.
    ▸ Don't blame me for the wrong wordings.
    ▸ Integration/Unit? Who cares!

    View Slide

  6. I'M HERE FOR A
    show-o .
    AMA

    View Slide

  7. I'M HERE FOR A
    discu ion.

    View Slide

  8. I MIGHT BE HERE FOR A
    FLAMEWAR.

    View Slide

  9. 1. NO-

    View Slide

  10. DEV === PROD === TEST
    USE SAME STACK AS IN PRODUCTION, I.E.
    DON'T TEST WITH SQLITE WHEN YOU'RE
    RUNNING PGSQL

    View Slide

  11. 2. NO-

    View Slide

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

    View Slide

  13. 3. NO-

    View Slide

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

    View Slide

  15. 4. NO-

    View Slide

  16. MOCK HTTP / EXTERNAL APIS

    View Slide

  17. 5. NO-

    View Slide

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

    View Slide

  19. View Slide

  20. 6.
    !
    - !

    View Slide

  21. USE DATAPROVIDERS AS MUCH AS POSSIBLE!

    View Slide

  22. 7.
    !
    - !

    View Slide

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

    View Slide

  24. ▸ we learned it the hard way.
    ▸ fixture_data.php
    ▸ fakeFactory FTW!

    View Slide

  25. 8.
    !
    -

    View Slide

  26. DO NOT TEST
    YOUR HTTP IMPLEMENTATION

    View Slide

  27. IT'S SLOW.

    View Slide

  28. ▸ Put stuff in non-http environment classes (e.g. repositories)
    ▸ Test extensively your repositories (all tests)
    ▸ Test vaguely your HTTP environment (basic tests)

    View Slide

  29. YOUR FUTURE YOU
    WILL BE THANKFUL.

    View Slide

  30. YOUR FUTURE YOU
    WILL BE VERY ANGRY.

    View Slide

  31. 9.
    !
    -

    View Slide

  32. MY LEGACY
    AKA: WTF AKA: OMG!

    View Slide

  33. ASSERT YOUR SQL

    View Slide

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

    View Slide

  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);
    }

    View Slide

  36. $this->assertSqlCount(0); //

    View Slide

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

    View Slide

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

    View Slide

  39. 9++.
    !
    -

    View Slide

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

    View Slide

  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);
    }

    View Slide

  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
    );

    View Slide

  43. WHY?

    View Slide

  44. WHY ASSERT SQL?
    ▸ Last Minute change introducing a lot of new sql statements?
    ▸ N+1 Problem: Gone!

    View Slide

  45. WHY NOT ASSERT SQL?
    ▸ Refactoring is a PITA
    ▸ Example?
    https://github.com/DieSocialisten/Swat.io-API-V2/pull/2164/files

    View Slide

  46. GIST LINK INCOMING!
    https://gist.github.com/johannesnagl/
    3bbfae042d85d8949f1cca2d1ce90539

    View Slide

  47. Bonus!

    View Slide

  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)

    View Slide

  49. ONE MOAR THING!

    View Slide

  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);
    }
    }
    }

    View Slide

  51. QUESTIONS?
    Answers!

    View Slide

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

    View Slide