Slide 1

Slide 1 text

Crafting Elegant Symfony Tests by Kevin Bond Image Credit: Piotr

Slide 2

Slide 2 text

Me? From Ontario, Canada Husband, father of three Symfony user since 1.0 Symfony Core Team Symfony UX Team @kbond on GitHub/Slack @zenstruck on Twitter

Slide 3

Slide 3 text

Writer At SymfonyCasts!

Slide 4

Slide 4 text

zenstruck? A GitHub organization where my open source packages live zenstruck/foundry zenstruck/browser zenstruck/messenger-test zenstruck/filesystem (wip) zenstruck/messenger-monitor-bundle (wip) ... Many co-maintained by Nicolas PHILIPPE ( @nikophil )

Slide 5

Slide 5 text

What we'll cover Testing helper libraries My testing style Combining above libraries in a test Symfony Testing Initiative Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 6

Slide 6 text

Testing Helper Libraries Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 7

Slide 7 text

foundry composer require --dev zenstruck/foundry https://github.com/zenstruck/foundry Object (ORM entity/Mongo document) factories One factory class per object Fixture loading Reset database between tests Repository assertions Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 8

Slide 8 text

foundry bin/console make:factory Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 9

Slide 9 text

foundry namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class FoundryTest extends KernelTestCase { use Factories, ResetDatabase; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 10

Slide 10 text

foundry public function test_user_registration(): void { UserFactory::assert()->empty(); // logic to register a user (with email: [email protected])... UserFactory::assert() ->count(1) ->exists(['email' => '[email protected]']) ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 11

Slide 11 text

foundry public function test_user_profile_update(): void { $user = UserFactory::createOne(['email' => '[email protected]']); // logic to update a user's email (to: [email protected])... // $user is auto-refreshed! $this->assertSame('[email protected]', $user->getEmail()); } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 12

Slide 12 text

foundry 2.0 (coming soon) Full rewrite and code cleanup 1.x to 2.x migration path (including rector rules) Real proxies using Symfony's Lazy Proxies Option for different factory types: Plain Object (no persistence) Persistent Object (not proxied) Proxied Object Array? Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 13

Slide 13 text

browser composer require --dev zenstruck/browser https://github.com/zenstruck/browser Fluent API for functional/E2E tests Json selector API (using JMESPath) Option to use Panther for JS testing Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 14

Slide 14 text

browser namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Browser\Test\HasBrowser; final class BrowserTest extends KernelTestCase { use HasBrowser; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 15

Slide 15 text

browser public function test_user_registration(): void { $this->browser() ->visit('/register') ->fillField('Name', 'Kevin') ->fillField('Email', '[email protected]') ->clickAndIntercept('Register') ->assertRedirectedTo('/profile') ->assertSuccessful() ->assertSeeIn('h1', 'Welcome Kevin!') ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 16

Slide 16 text

browser public function test_api_get_podcast_endpoint(): void { $this->browser() ->get('/podcast/1') ->assertJson() ->assertJsonMatches('metadata.title', 'Podcast 1') // JMESPath ->assertJsonMatches('metadata.duration', 2325) ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 17

Slide 17 text

browser public function test_podcast_player(): void { $this->pantherBrowser() ->visit('/player') ->click('Play Podcast 1') ->waitUntilSeeIn('.player', 'Playing Podcast 1') ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 18

Slide 18 text

browser 2.0 (coming soon) Total rewrite of the DOM selector/manipulation API which will allow for more powerful element selections. Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 19

Slide 19 text

browser 2.0 (coming soon) $browser->click(function(Dom $dom) { return $dom->find('.product-table td:contains("Product 1")') ->closest('tr') ->descendant(Selector::link('Edit')) ; }); Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 20

Slide 20 text

messenger-test composer require --dev zenstruck/messenger-test https://github.com/zenstruck/messenger-test Assert that messages are dispatched/queued/handled Process queued messages (either automatically or on-demand) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 21

Slide 21 text

messenger-test # config/packages/messenger.yaml when@test: framework: messenger: transports: async: test:// Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 22

Slide 22 text

messenger-test namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Messenger\Test\InteractsWithMessenger; final class MessengerTest extends KernelTestCase { use InteractsWithMessenger; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 23

Slide 23 text

messenger-test public function test_processing_podcasts(): void { $this->transport()->queue()->assertEmpty(); // optional // ...operation that adds ProcessPodcast message to your queue $this->transport()->queue() ->assertCount(1) ->assertContains(ProcessPodcast::class) ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 24

Slide 24 text

messenger-test // ... // process first message on the queue $this->transport()->processOrFail(1); Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 25

Slide 25 text

messenger-test // ... $this->transport()->dispatched() ->assertCount(1) ->assertContains(ProcessPodcast::class) ; Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 26

Slide 26 text

messenger-test // ... $this->transport()->queue() ->assertCount(2) ->assertContains(SendToApple::class, times: 1) ->assertContains(SendToSpotify::class, times: 1) ; Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 27

Slide 27 text

mailer-test composer require --dev zenstruck/mailer-test https://github.com/zenstruck/mailer-test Alternative to Symfony's KernelTestCase::assertEmail*() methods Fluent API for testing sent emails (count, recipients, etc.) Per-message assertions (cc, subject, body content, etc.) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 28

Slide 28 text

mailer-test when@test: framework: mailer: dsn: null://null # optional but improves test speed Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 29

Slide 29 text

mailer-test namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Mailer\Test\InteractsWithMailer; final class MailerTest extends KernelTestCase { use InteractsWithMailer; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 30

Slide 30 text

mailer-test public function test_contact_form(): void { // ... some operation that sends an email to [email protected] $this->mailer() ->assertSentEmailCount(1) ->assertEmailSentTo('[email protected]', function(TestEmail $email) { // ... }) ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 31

Slide 31 text

mailer-test ->assertEmailSentTo('[email protected]', function(TestEmail $email) { $email ->assertSubject('Contact Form Submission') ->assertContains('Thanks for getting in touch!') // both text and html ->assertHasFile('cats.png', 'image/png') ->assertHasTag('contact-form') ; }) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 32

Slide 32 text

mailer-test $this->mailer()->sentEmails() ->whereSubject('Contact Form Submission') // filters ->whereReplyTo('[email protected]') ->assertCount(1) ->first() ->assertContains('Thanks for getting in touch!') ->assertHasFile( expectedFilename: 'cats.png', expectedContentType: 'image/png', expectedContents: file_get_contents(FIXTURES_DIR.'/cats.png') ) ; Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 33

Slide 33 text

mailer-test 2.0 (coming soon) DOM-based HTML email content assertions (similar to zenstruck/browser ). For example, this will enable you to assert that your HTML email's h1 tag contains specific text. Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 34

Slide 34 text

My Testing Style Readable Self-contained Outside-in AAA: Arrange (configure the current test's world state) Pre-Assert (optional, setup expectations) Act (perform the operation) Assert (verify the operation's result) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 35

Slide 35 text

My Test Structure tests/Unit extends TestCase follows src/ directory structure tests/Integration extends KernelTestCase follows src/ directory structure Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 36

Slide 36 text

My Test Structure tests/Functional extends WebTestCase (or KernelTestCase ) named/grouped by feature (e.g. User\RegistrationTest.php ) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 37

Slide 37 text

All together now! final class UploadPodcastTest extends KernelTestCase { use InteractsWithMessenger; use InteractsWithMailer; use HasBrowser; use Factories, ResetDatabase; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 38

Slide 38 text

All together now! public function test_upload_podcast_flow(): void { PodcastFactory::assert()->empty(); // pre-assertion $browser = $this->browser() ->actingAs(UserFactory::createOne(['email' => '[email protected]'])) ->visit('/podcasts/upload') ->fillField('Title', 'First Week at SymfonyCasts') ->attachFile('File', FIXTURE_DIR.'/podcasts/62.mp3') ->click('Upload') ->assertRedirectedTo('/podcasts') ->assertSee('First Week at SymfonyCasts (processing)') ; // ... Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 39

Slide 39 text

All together now! // ... $podcast = PodcastFactory::first(); $this->assertSame('First Week at SymfonyCasts', $podcast->getTitle()); $this->assertFalse($podcast->isProcessed()); $this->assertNull($podcast->getDuration()); $this->mailer()->assertEmailSentTo( '[email protected]', 'Processing Podcast "First Week at SymfonyCasts"', ); // ... Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 40

Slide 40 text

All together now! // ... $this->transport() ->queue()->assertContains(ProcessPodcast::class, times: 1)->back() ->processOrFail() ; $this->assertTrue($podcast->isProcessed()); $this->assertSame(62, $podcast->getDuration()) // ... Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 41

Slide 41 text

All together now! // ... $this->mailer()->assertEmailSentTo( '[email protected]', 'Successfully Processed Podcast "First Week at SymfonyCasts"', ); $browser ->visit('/podcasts') ->assertSee('First Week at SymfonyCasts (processed)') ; Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 42

Slide 42 text

More Testing Helper Libraries Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 43

Slide 43 text

console-test composer require --dev zenstruck/console-test https://github.com/zenstruck/console-test Alternate to Symfony's CommandTester Fluent API to have your console tests read like a story Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 44

Slide 44 text

console-test namespace App\Tests; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Zenstruck\Console\Test\InteractsWithConsole; final class UpdatePodcastsCommandTest extends KernelTestCase { use InteractsWithConsole; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 45

Slide 45 text

console-test public function test_can_update_all_podcasts(): void { $this->executeConsoleCommand('podcast:update --all') ->assertSuccessful() ->assertOutputContains('Updating all podcasts...') ->assertOutputContains('Done!') ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 46

Slide 46 text

console-test public function test_can_update_single_podcast_interactively(): void { $this->executeConsoleCommand( 'podcast:update', inputs: ['Podcast 1', 'apple']) ->assertSuccessful() ->assertOutputContains('Updating Podcast 1...') ->assertOutputContains('Sending to Apple...') ->assertOutputNotContains('Sending to Spotify...') ->assertOutputContains('Done!') ; } Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 47

Slide 47 text

dependency-injection-test composer require --dev matthiasnoback/symfony-dependency- injection-test https://github.com/SymfonyTest/SymfonyDependencyInjectionTest Test bundle extensions Test bundle configuration - especially useful if your bundle has complex configuration (normalizers and validators) Compiler passes Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 48

Slide 48 text

Symfony Testing Initiative "This should be in Symfony Core" zenstruck/* considered POC Goal: make Symfony testing easier Help from Nicolas PHILIPPE ( @nikophil ) Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond

Slide 49

Slide 49 text

Thank You! Crafting Elegant Symfony Tests Kevin Bond • @zenstruck • github.com/kbond