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

Unit Testing Workshop

Unit Testing Workshop

This is the presentation of my unit testing workshop.

See https://github.com/lukaslangen/workshop-unittesting-presentation for sources

Avatar for Lukas Langen

Lukas Langen

May 12, 2021

Other Decks in Programming

Transcript

  1. Introduction This is a workshop about automated testing with the

    focus on unit testing and Test-driven development Modules 1. Basics 2. Techniques of Unit Testing and TDD 3. Unit Testing in legacy software Each module ends with a practice example After each practice example there will be a break. By Lukas Langen ([email protected]) 2
  2. Module 1: Basics Part 1: Theory Automated testing overview Terminology

    White-Box vs Black-Box testing What is unit testing and why would you want to use it? What is TDD and why would you want to do practice it? Part 2: First practice example Presentation of the first practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 3
  3. Module 2: Techniques of Unit Testing Part 1: Techniques of

    Unit Testing and TDD Writing testable code Test Doubles Organizing tests Part 2: Second practice example Presentation of the second practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 4
  4. Module 3: Unit Testing in legacy Software Part 1: Unit

    Testing in Legacy software What is legacy software in the context of Unit testing? Techniques to get legacy code tested Part 2: Third practice example Presentation of the third practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 5
  5. Module 1: Basics - Overview Part 1: Theory Automated testing

    overview Terminology White-Box vs Black-Box testing What is unit testing and why would you want to use it? What is TDD and why would you want to do practice it? Part 2: Practice Presentation of the practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 8
  6. Module 1 - Automated testing Different use cases require different

    kind of tests Three main kinds of testing: Unit, Integration, End-2-End By Lukas Langen ([email protected]) 9
  7. Module 1 - Terminology System-Under-Test (SUT): The system you want

    to test (eg. a single unit or a whole system) Unit-Under-Test: The SUT in the context of a unit test Unit: A small component of an application. Usually a class, method, or function. In Unit testing also SUT White-Box-Testing: The test case knows about the inner workings of a SUT Black-Box-Testing: The test case does not know about the inner workings of a SUT Assertion: A check if an expectation was true. Fixture: The known state of the application or world, that needs to be set up before a test Test Doubles: Stand-In classes for actual dependencies of the SUT. More on this topic in Module 2 Data Provider: Provide input data and expectations for the input data. Used to test more cases with the same test By Lukas Langen ([email protected]) 10
  8. Module 1 - White Box vs Black Box Testing When

    to use White-Box-Testing? When the SUT doesn't return a value When a test has to make sure specific methods on a dependency are called When the behaviour of a SUT should be tested Techniques for White-Box-Testing will be discussed in the second module When to use Black-Box-Testing? When the SUT returns a value When a test doesn't need to make sure specific methods on a dependency are called When the resulting state of a SUT should be tested By Lukas Langen ([email protected]) 11
  9. Module 1 - What is unit testing? Unit Tests test

    small components of a software They test them in isolation of the rest of the software They are fast They are automated By Lukas Langen ([email protected]) 12
  10. Module 1 - Why would you want to use Unit

    Testing? Code becomes more reliable Reduces code complexity Bugs are found easier and quicker Quality of code rises Give documentation By Lukas Langen ([email protected]) 13
  11. Module 1 - What is Test-driven development? By Lukas Langen

    ([email protected]) Test-driven development (TDD) is a sofware development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases. This is opposed to software being developed first and test cases created later. -- Wikipedia “ “ 14
  12. Module 1 - What is Test-driven development? Tests are written

    before implementations Only one requirement is tested, then implemented, repeating until all requirements are implemented By Lukas Langen ([email protected]) 15
  13. Module 1 - Why would you want to practice TDD?

    Focus shifts from implementation to usability Simplifies code Results in more modular code, which makes software easier to extend Documentation is always up-to-date Shortens development time, because more there's more focus on accomplishing goals By Lukas Langen ([email protected]) 16
  14. Module 1 - First practice example Setup Clone the practice

    repository: git clone [email protected]:lukaslangen/workshop-unittesting-practice-examples.git Checkout the first-example tag: git checkout first-example By Lukas Langen ([email protected]) 17
  15. Module 1 - First practice example Task Implement the FirstExample::getMonths()

    method by using a test-driven approach You may need to use the following techniques and methods and classes: \PhpUnit\Framework\TestCase::assertEquals() \PhpUnit\Framework\TestCase::assertCount() \Lcobucci\FrozenClock::class dataProviders By Lukas Langen ([email protected]) 18
  16. Module 2: Techniques of Unit Testing Part 1: Techniques of

    Unit Testing and TDD Writing testable code Test Doubles Organizing tests Part 2: Second practice example Presentation of the second practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 22
  17. Module 2 - Writing testable code No side effects: A

    unit should not be the cause of side effects like I/O. Extraction of non-testables into Wrappers: I/O operations, database connections, etc. should be extracted in wrapper classes. This solves the "No side effects" IoC / DI: Classes should adhere to the IoC principle by implementing the DI pattern. Single Responsibility: A unit should be short and do only one thing Sparse usage of static: Static methods are hard to mock. Static variables have global state and should be avoided where possible Test Driven Development: If code is developed using TDD the implementation is testable by design. By Lukas Langen ([email protected]) 23
  18. Module 2 - Test Doubles Test Doubles are frequently used

    in unit testing. There are different types of test doubles: Dummy: An object that is passed into a class, but never used in the context of the specific test case. Fake: An object that has a working implementation, but takes shortcuts designed for testing, not for production code. Stub: An object that provides known return values for calls made during the test. Spy: A stub that also records information on how they are called. Mock: An object that is pre-programmed with expectations for the unit under test's behaviour Test Doubles are at least one or multiple of the above, with the exception of dummies. A dummy can only be a dummy. By Lukas Langen ([email protected]) 24
  19. Module 2 - Test Doubles Examples class SomeValueObject { public

    $id; public $value; } interface Repository { public function save(SomeValueObject $object): void; public function read(int $id): SomeValueObject; } class UnitUnderTest { public function __construct(Repository $repository) {} public function doStuffThatNeedsRepository(): void {} public function doStuffThatDoesntNeedRepository(): void {} } By Lukas Langen ([email protected]) 25
  20. Module 2 - Test Doubles Dummy Example class DummyRepository implements

    Repository { public function save(SomeValueObject $object): void {} public function read(int $id): SomeValueObject { return new SomeValueObject(); } } // In the test: $uut = new UnitUnderTest(new DummyRepository()); $uut->doStuffThatDoesntNeedRepository(); By Lukas Langen ([email protected]) 26
  21. Module 2 - Test Doubles Fake Example class FakeRepository implements

    Repository { private $store = []; public function save(SomeValueObject $object): void { $this->store[$object->id] = $object; } public function read(int $id): SomeValueObject { return $this->store[$id]; } } // In the test: $uut = new UnitUnderTest(new FakeRepository()); $uut->doStuffThatNeedsRepository(); By Lukas Langen ([email protected]) 27
  22. Module 2 - Test Doubles Stub Example class StubRepository implements

    Repository { public function save(SomeValueObject $object): void { // do nothing } public function read(int $id): SomeValueObject { $object = new SomeValueObject(); $object->id = $id; $object->value = 'some value'; return $object; } } // In the test: $uut = new UnitUnderTest(new StubRepository()); $uut->doStuffThatDoesntNeedRepository(); By Lukas Langen ([email protected]) 28
  23. Module 2 - Test Doubles Spy Example class SpyRepository implements

    Repository { public $callCount = 0; public function save(SomeValueObject $object): void { $this->callCount++; } public function read(int $id): SomeValueObject { return new SomeValueObject(); } } // In the test $spy = new SpyRepository(); $uut = new UnitUnderTest($spy); $uut->doStuffThatNeedsRepository(); $this->assertSame(3, $spy->callCount); By Lukas Langen ([email protected]) 29
  24. Module 2 - Test Doubles Mocks Mocks are more complex

    than any of the preceding doubles. Examples are only in phpunit: Asserting that Repository::save() is called once and Repository::read() is called exactly twice // Create mock $mock = $this->createMock(Repository::class); // Set expectations $mock->expects($this->once())->method('save'); $mock->expects($this->exactly(3))->method('read'); $uut = new UnitUnderTest($mock); $uut->doStuffThatNeedsRepository(); By Lukas Langen ([email protected]) 30
  25. Module 2 - Test Doubles Mocks Asserting that Repository::class 's

    methods are never called // Create mock $mock = $this->createMock(Repository::class); // Set expectations $mock->expects($this->never())->method('save'); $mock->expects($this->never())->method('read'); $uut = new UnitUnderTest($mock); $uut->doStuffThatDoesntNeedRepository(); By Lukas Langen ([email protected]) 31
  26. Module 2 - Test Doubles Stubs in phpunit Stubs work

    pretty much the same way as mocks in phpunit, with the difference, that you can't set expectations // Create stub $stub = $this->createStub(Repository::class); // Set return values $return = new SomeValueObject(); $return->id = 1; $return->value = 'some value'; $stub->method('read')->willReturn($return); $uut = new UnitUnderTest($stub); $uut->doStuffThatNeedsRepository(); By Lukas Langen ([email protected]) 32
  27. Module 2 - Test Doubles When to use doubles and

    which double to use? Answer: It depends Different philosophies of testing: Classical TDD vs Mockist TDD Classical TDD: Use real objects wherever possible (also if it's not awkward to use the real object) Mockist TDD: Always create a double for each and every dependency with interesting behaviour Other test doubles in PhpUnit PhpUnit doesn't immediately provide implementations for dummies, fakes or spies. PhpUnit Stubs and Mocks implement the behaviour of fakes and spies as well! By Lukas Langen ([email protected]) 33
  28. Module 2: Organizing Tests Where to put tests? They should

    usually live in a test(s)/ directory in the root of a project and mirror a src/ or module/ directory How to structure Tests? A test class should usually be responsible for testing one unit of code. If the unit is a class with one public method, than there should be one test class, that's responsible for testing the whole class. If a class has multiple public methods each method should be considered the unit and therefore each public method has their own test class. Reccommendation: Always create a test class for each public method, even if it is the only public method. This makes your tests future proof if another public method will be added. By Lukas Langen ([email protected]) 34
  29. Module 2: Organizing tests How to organize test doubles? Depending

    on the situation one might want to share some test doubles. One way of achieving this is to create a static factory class, that handles this interaction. Sticking with the previous example one such factory could look like this: class RepositoryDoubleFactory { public static function createDummy(): DummyRepository {...} public static function createFake(): FakeRepository {...} public static function createSpy(): SpyRepository {...} /** @return Repository|Stub */ public static function createStub(\PhpUnit\Framework\TestCase $testCase) {...} /** @return Repository|MockObject */ public static function createMock(\PhpUnit\Framework\TestCase $testCase) {...} } By Lukas Langen ([email protected]) 35
  30. Module 2: Organizing tests How to instantiate unit under test?

    Experience has shown, that not all dependencies have to be specifically defined for a test case. Also it's often a lot of work to change all instantiations if the constructer of the unit under test changes. A solution for this is a private factory method in the test class, that abstracts the actual instantiation away: private function createUnitUnderTest(?Repository $repository = null): UnitUnderTest { return new UnitUnderTest($repository ?? RepositoryDoubleFactory::createDummy()); } By Lukas Langen ([email protected]) 36
  31. Module 2: Organizing Tests How many assertions? Ideally there should

    be only one assertion per test case. Often times that is not feasible, so it is fine if there are multiple assertions per test case. By Lukas Langen ([email protected]) 37
  32. Module 2 - Second practice example Setup Reverse local changes:

    git checkout -- ./ Checkout the second-example tag: git checkout second-example By Lukas Langen ([email protected]) 38
  33. Module 2 - Second practice example Task Implement the SecondExample::create()

    method by using a test-driven approach You may need to use the following techniques and methods and classes: Test Doubles (try to use the appropriate double for your test cases) By Lukas Langen ([email protected]) 39
  34. Module 3: Unit Testing in legacy Software Part 1: Unit

    Testing in Legacy software What is legacy software in the context of Unit testing? Techniques to get legacy code tested Part 2: Third practice example Presentation of the third practice example Practice (~ 30 minutes) Discussion and presentation phase, including "model solution" By Lukas Langen ([email protected]) 43
  35. Module 3 - What is legacy software? Untestable and untested

    code (Multiple) Side effects No Inversion of Control No Dependency Injection Units have multiple responsibilities Global state is used A lot of complexity By Lukas Langen ([email protected]) 44
  36. Module 3 - Techniques to get legacy code tested Do

    not worry to not get it "right" Your solutions won't be perfect. To get the code tested a lot of refactoring is neccessary. Try to do this in an iterative manner and not everything at once. If you try to do everything at once usually things will break. Keep backwards compatibility in mind While refactoring, make sure you don't accidentally break backwards compatibility. The behaviour of a method should stay the same. By Lukas Langen ([email protected]) 45
  37. Module 3 - Techniques to get legacy code tested Wrap

    non-testable code Code to test: // ... $somePostParam = $_POST['some-post-param']; // ... Wrapper: class PostParams { public function getParam(string $name) { return $_POST[$name]; } } By Lukas Langen ([email protected]) 46
  38. Module 3 - Techniques to get legacy code tested Wrap

    non-testable code Code to test: // ... $users = (new \PDO('some-dsn'))->query('SELECT * FROM users')->fetchAll(); // ... Wrapper: class UserRepository { /** @var \PDO */ private $pdo; public function getUsers(): array { return $this->pdo->query('SELECT * FROM users')->fetchAll(); } } By Lukas Langen ([email protected]) 47
  39. Module 3 - Techniques to get legacy code tested Wrap

    side effects Code to test: // ... $email = '[email protected]' $id = 1; (new \PDO('some-dsn'))->exec("UPDATE users SET email=$email WHERE id=$id"); // ... Wrapper: class UserRepository { /** @var \PDO */ private $pdo; public function setEmail(int $id, string $email): void { $this->pdo->exec("UPDATE users SET email=$email WHERE id=$id"); } } By Lukas Langen ([email protected]) 48
  40. Module 3 - Techniques to get legacy code tested Introduce

    (optional) DI Code to test: class UnitUnderTest { private function getUserRepository(): UserRepository { return new UserRepository(); } } Optional DI: class UnitUnderTest { private $userRepository; public function __construct(?UserRepository $repository = null) { $this->userRepository = $repository ?? new UserRepository(); } private function getUserRepository(): UserRepository { return $this->userRepository; } } By Lukas Langen ([email protected]) 49
  41. Module 3 - Techniques to get legacy code tested Break

    down code to adhere to Single-Responsibility principle If you see functions / methods / classes, that do multiple things at once, try to break them down into smaller, single responsible functions / methods / classes By Lukas Langen ([email protected]) 50
  42. Module 2 - Second practice example Setup Reverse local changes:

    git checkout -- ./ Checkout the third-example tag: git checkout third-example By Lukas Langen ([email protected]) 51
  43. Module 3 - Third practice example Task Refactor and test

    ThirdExample::updateEmailForUser() Make use of the presented techniques to do so. By Lukas Langen ([email protected]) 52
  44. Sources Image of testing pyramid: https://betterprogramming.pub/the-test-pyramid-80d77535573 Image of TDD cycle:

    https://brainhub.eu/blog/test-driven-development-tdd/ Module 2 - Test Doubles: https://martinfowler.com/articles/mocksArentStubs.html By Lukas Langen ([email protected]) 55