Lumped tweets

Just marks

Easy to write tests with Constructor Injection for PHP

Excuse

This page is translation for my self previous article as a my English training of the following link. Let correct my wrong English/strange explainings in comment or my Twitter

Constructor Injection でやるテストしやすいPHP - Twitter以上ブログ以下

twitter.com

Background

We should write test code for each implementation. However, If you write static function, You face a problem that is difficult written to test codes.

Because, if static function depends on other classes, you have to care about logic for other classes.

So, I want you to know how to write easily testable code by Constructor Injection.

Table of Contents

  • What Constructor Injection?
  • Example code
  • Pro/Cons
  • Conclusion

What Constructor Injection?

First, we should be known to Dependency Injection before learning what about Constructor Injection.

Dependency Injection

Dependency Injection is a way to realize the SOLID principle.

An object can ignore how an injected object is implemented and use only their knowledge/behavior. As a result, An object can focus on describing themself behavior.

DependencyInjection has the following positive effects on your code. * Increase the flexibility of being configurable. * Decrease the cost of writing the test code. * Become independent of external systems/frameworks.

more description wrote in https://en.wikipedia.org/wiki/Dependency_injection#Three_types_of_dependency_injection

Constructor Injection

Constructor Injection is one of the implementation methods for Dependency Injection.

In PHP, for example for following code.

<?php
class K {
  // over 7.4, we can declare property type.
  private Dependency $dependency;
  public function __constructor(Dependency $dependency)
  {
    $this->dependency = $dependency;
  }
}

Okay. It's easy. If you still using old PHP version, you should declare type on way of PHP Doc( /* @var Dependency */ )

Column: Efficient Dependency Injection.

In the more higher design of Dependency Injection, They don't use Dependency as a concreted class. Than the right way that instead of DependencyInterface/AbstractDependency as the abstractions. For example,

<?php

interface DependencyInterface {
  public function awesomeFunction();
}

class K {
  // over 7.4, we can declare property type.
  private DependencyInterface $dependency;
  public function __constructor(DependencyInterface $dependency)
  {
    $this->dependency = $dependency;
  }
}

It's a good way than my first example code. However, I don't use Interface in this article because of the amount of description line increases.

let write code using by Constructor Injection

Basically, I case about only a few things of the

  • Don't new without controller class.
  • Receive a behavior known class in constructor.
  • A method wrote for depends on the argument and call to behavior known class.

The following sample code is a performing check to a transaction that should prevent or not by A/B testing and price threshold.

memo: LaunchDarkly is an A/B test provider https://launchdarkly.com/

<?php

class PreventHighPrice
{
  private const PRICE_THRESHOLD = 100;
  // LaunchDarkly is the A/B test provider.
  private LaunchDarklyService $ld_service;
  public function constructor(LaunchDarklyService $ld_service)
  {
    $this->ld_service = $ld_service;
  }

  public function prevent(User $user, Item $item): void
  {
    if (!$this->ld_service->isExperiment($user)) {
      return;
    }

    if ($item->getPrice() >= self::PRICE_THRESHOLD) {
      throw new PreventTransactionByHighPrice()
    }
  }
}

Okay. It's clear code. This class has two logic.

  • Is a user target on the A/B test?
    • If no, don't check the item price.
  • Is the item price over than equal threshold? *If yes, throw an Exception.

Also, let show test codes.

<?php
class PreventHighPriceTest extends PHPUnit\Framework\TestCase
{
  /**
   * @dataProvider testShouldPreventProvider
   */
  public function testShouldPrevent(bool $is_experiment, int $price, bool $expect): void
  {
    $user = new User()

    $ld_service = $this->createMock(LaunchDarklyService::class)
    $ld_service->method('isExperiment')->expects($this->once())->willReturn($is_experiment);

    $item = new Item(['price' => $price])

    $preventHighPrice = new PreventHighPrice($ld_service);
    $this->assertSame($expect, $preventHighPrice->shouldPrevent($user, $item))
  }

  public function testShouldPreventProvider(): array
  {
    return [
      'no experiment' => [false, 100, false],
      'in experiment lower price' => [true, 99, false],
      'in experiment higher price' => [true, 100, true]
    ]
  }
}

How do you think about this? I'd say test code is clear and test cases are satisfied for describes to expectation.

So, this test code can focus on describing what the function does and under what conditions/arguments.

If this class dependency will increase, the constructor arity is rising. And add a new Mock on test code.

However, This test code expects a class of mocked objects has satisfied for tests. Because what happens when a function is called with arguments is a black box.

Column: Single responsibility principle.

A class should have responsibilities expressed in one concise sentence.

I usually split a class behavior for small classes and build one behavior from dependencies by injection.

https://en.wikipedia.org/wiki/Single_responsibility_principle

Pro/Cons

Pro

  • Easy to write test code.
  • Easy to explain complicated conditions on test code.
  • When the depends class of internal behavior will change, it can ignore that if returning type won't change.

Cons

  • Mock is a mock. In PHP unit cannot catch when the changes of return types by dependency class's function.

That's issue already solved by new version's PHPUnit. We should increase PHPUnit version if you have time!

github.com

Conclusion

If you are worried about how to write test code for your facing class, you can separate that behavior and declare other class and inject that complexity behavior from your facing class's constructor.

After you choose the Dependency Injection way, you can get more clear code, and testing is functional.