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
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
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
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
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
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
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
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
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
([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
before implementations Only one requirement is tested, then implemented, repeating until all requirements are implemented By Lukas Langen ([email protected]) 15
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
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
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
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
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
$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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
(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
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