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

Practical TDD

Practical TDD

Test Driven Development. A given for many software engineers, but a lot of php developers are still only prodding at it with a stick from a safe distance.

This talk won't be an introduction to TDD. I'm gonna try to take away this fear by giving opinionated answers to the Difficult Questions. I hope it will lead to some good discussions at the end.

1. What is the purpose of TDD?
2. What are the pitfalls of TDD?
3. How do I TDD with the database?
4. How do I TDD with web services?
5. How do I TDD with the front end?
6. How do I stick to TDD when the deadline is approaching?
7. How much code coverage should we aim for?
8. How long may a test suite run?
9. How can I start with TDD in a legacy code base?
10. How to convince my boss TDD is good?

Speaker: Bas de Ruiter

Bas is software architect at Harver. He's been an Oracle and later PHP developer for about 12 years. Besides his enthusiasm for domain driven design, he is a big fan of test driven development. He has set it up and used it for all projects he has worked on in the past five years.

Avatar for Bas de Ruiter

Bas de Ruiter

December 15, 2016

Other Decks in Programming

Transcript

  1. Difficult Questions 1. What is the purpose of TDD? 2.

    What are the pitfalls of TDD? 3. How do I TDD with the database? 4. How do I TDD with web services? 5. How do I TDD with the front end?
  2. Difficult Questions 6. How do I stick to TDD when

    the deadline is approaching? 7. How much code coverage should we aim for? 8. How long may a test suite run? 9. How can I start with TDD in a legacy code base? 10. How to convince my boss TDD is good?
  3. Who the hell do I think I am? • Bas

    de Ruiter — @winkbrace • Software Architect at Harver • Started as Oracle developer, doing PHP professionally for about 8 years now. • Been using TDD for all my projects for over 5 years.
  4. What is TDD? • Test Driven Development • Write test

    first, before the code • Red, Green, Refactor • Very short feedback loop
  5. /** @test */
 public function it_should_store_a_user()
 {
 // Arrange
 $user

    = new User('John Doe', '[email protected]');
 
 // Act
 $this->repo->save($user);
 
 // Assert
 $actual = $this->repo->fetchByEmail('[email protected]');
 $this->assertEquals($actual, $user);
 }
  6. What is the purpose of TDD? • It drives design

    • It documents your code • It makes your code agile
  7. TDD drives design • You think upfront how to use

    your new code. • You always start at the requirements • Single Responsibility and Dependency Inversion • loose coupling • high cohesion
  8. Single Responsibility & Dependency Inversion • Example case: NewsletterMailer •

    I want to test it • without actually fetching users from the database • without actually sending emails
  9. class NewsletterMailerTest extends \PHPUnit_Framework_TestCase
 {
 private $newsletterMailer;
 
 /** @test

    */
 public function it_should_mail_a_user()
 {
 $this->newsletterMailer->mail( new User('Bas de Ruiter’, '[email protected]') );
 
 // TODO assert mail was sent
 }
 
 /** @test */
 public function it_should_mail_all_users()
 {
 $this->newsletterMailer->mailAll();
 }
 }
  10. final class NewsletterMailer
 {
 private $mailer;
 private $userRepo;
 
 public

    function __construct( MailerInterface $mailer, UserRepository $userRepo )
 {
 $this->mailer = $mailer;
 $this->userRepo = $userRepo;
 } …
  11. … public function mailAll()
 {
 foreach ($this->userRepo->fetchSubscribers() as $user) {


    $this->mail($user);
 }
 }
 
 public function mail(User $user)
 {
 $this->mailer->send($user->email, 'newsletter.html');
 }

  12. public function setUp()
 {
 $user1 = new User('John Doe', '[email protected]');


    $user2 = new User('Susan Doe', '[email protected]');
 
 $userRepo = \Mockery::mock(UserRepository::class);
 $userRepo->shouldReceive(‘fetchSubscribers') ->andReturn([$user1, $user2]);
 
 $this->mailer = new StubMailer(); $this->newsletterMailer = new NewsletterMailer( $this->mailer, $userRepo);
 }

  13. final class StubMailer implements MailerInterface
 {
 private $emails = [];


    
 public function send(string $email, string $template)
 {
 $this->emails[$email][] = $template;
 }
 
 public function sentEmailTo(string $email) : bool
 {
 return array_key_exists($email, $this->emails);
 }
 }
  14. /** @test */
 public function it_should_mail_a_user()
 {
 $this->newsletterMailer->mail( new User('Bas

    de Ruiter', '[email protected]'));
 
 $this->assertTrue( $this->mailer->sentEmailTo('[email protected]'));
 }
 
 /** @test */
 public function it_should_mail_all_users()
 {
 $this->newsletterMailer->mailAll();
 
 $this->assertTrue( $this->mailer->sentEmailTo('[email protected]'));
 $this->assertTrue( $this->mailer->sentEmailTo('[email protected]'));
 }
  15. What are the pitfalls of TDD? • Misunderstanding TDD with

    writing a test suite • Going overboard with testing in isolation • Mocking
  16. Mocking • Avoid mocking where possible. A mock is not

    real code. • A mock that returns a mock is always evil. • Stubs offer better documentation and are real code. • Only mock repositories or web services. • Or mock to exploratory develop interface
  17. How do I TDD with the database? • Use sqlite

    for unit tests • Use migrations and seeds • Use repositories for all database interaction • Never mock the class you are developing!
  18. bootstrap.php sqlite bootstrapping vendor/bin/phpunit run all migrations in in-memory sqlite

    copy base- testdb.sqlite to testdb.sqlite run main seeds run unit tests load specific seeds using traits
  19. <?php declare(strict_types = 1);
 
 require_once __DIR__ . '/../vendor/autoload.php';
 


    define('CONFIG_PATH', __DIR__ . '/../config');
 define('TESTS_PATH', __DIR__);
 
 putenv('DB_CONNECTION=sqlite');
 putenv('DB_DATABASE=' . TESTS_PATH . '/testdb.sqlite');
 
 function reset_test_db()
 {
 chdir(TESTS_PATH);
 copy('stubs/stub-testdb.sqlite', 'testdb.sqlite');
 chdir(TESTS_PATH . '/..');
 }
 reset_test_db(); bootstrap.php
  20. How do I TDD with web services? • First create

    the request in a web service tool. • Then develop your consumer class, using a real connection • Then replace the connection with a mock that will return the exact same responses
 (Really easy with guzzle)
  21. trait MocksGuzzle
 {
 protected $guzzle;
 
 protected function mockGuzzleWithResponses(array $bodies)


    {
 $responses = [];
 foreach ($bodies as $body) {
 $responses[] = new Response(200, [], $body);
 }
 $handler = new MockHandler($responses);
 
 $this->guzzle = new Guzzle([ 'handler' => HandlerStack::create($handler) ]);
 }
 }
  22. use MocksGuzzle; public function setUp()
 {
 $login = json_encode(['result' =>

    'sessionId']);
 $vacancy = json_encode(['result' => [
 "uid" => "1234567",
 “name" => “Test vacancy",
 "entryDateTime" => "20161017T14:35:19",
 ]]);
 $logout = json_encode(['result' => true]); 
 $this->mockGuzzleWithResponses([ $login, $vacancy, $logout ]);
 
 $this->logger = new StubLogger();
 $this->requester = new OtysVacancyRequester( $this->logger, $this->guzzle);
 }
  23. How do I stick to TDD when the deadline is

    approaching? write test after write test first
  24. How much code coverage should we aim for? • Measurement

    for stability and agility • Has nothing to do with TDD • Actually distracting from design • But I confess I put all simple getters (and setters) in a test_creation() method for the coverage
  25. public function setUp()
 {
 $this->options = ['base_uri' => 'https://ex.com/api/v2/'];
 $this->request

    = new GuzzleRequest( HttpMethod::PATCH(), 'candidate', $this->options);
 }
 
 public function test_creation()
 {
 $this->assertEquals('PATCH', $this->request->getMethod());
 $this->assertEquals('candidate', $this->request->getUri());
 $this->assertEquals($this->options, $this->request->getOptions());
 }
  26. How long may a test suite run? • under 1

    second perfect • above 8 seconds becomes annoying. 
 Then run only selection during development:
 t tests/unit/Mail
  27. How do I convince my boss that TDD is good?

    • development takes 20% less time in long run. • at least 50% less bugs • agility improves • robustness improves • developer happiness:
 more pleasurable to work in well organised code.
  28. What should you remember? • TDD is a tool that

    helps you build code that is easy to change. • The product of TDD is not the test suite • mocks are not real code • It takes time to setup a test environment, 
 so steal my best practices.