DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
View Events Video Library
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Integrating PostgreSQL Databases with ANF: Join this workshop to learn how to create a PostgreSQL server using Instaclustr’s managed service

Mobile Database Essentials: Assess data needs, storage requirements, and more when leveraging databases for cloud and edge applications.

Monitoring and Observability for LLMs: Datadog and Google Cloud discuss how to achieve optimal AI model performance.

Automated Testing: The latest on architecture, TDD, and the benefits of AI and low-code tools.

Related

  • Combining gRPC With Guice
  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • The Singleton Design Pattern
  • Micronaut With Relation Database and...Tests

Trending

  • Spring WebFlux Retries
  • Microservices With Apache Camel and Quarkus (Part 5)
  • Next.js vs. Gatsby: A Comprehensive Comparison
  • LTS JDK 21 Features
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Practical PHP Testing Patterns: Dependency Injection

Practical PHP Testing Patterns: Dependency Injection

Giorgio Sironi user avatar by
Giorgio Sironi
·
May. 16, 11 · Interview
Like (0)
Save
Tweet
Share
2.02K Views

Join the DZone community and get the full member experience.

Join For Free
Dependency Injection has been one of the most spoken maxim in the PHP world in the last year. We already said a lot of things about it, and all tests in this series have used it in order to be fast and to the point. In this article we'll take a look at it from the testability point of view: this technique is fundamental for every unit test you will write.

Unit testing, in fact, requires to isolate an object, or a small group of objects. These Systems Under Test have to provide a seam where we can substitute the real object, used in production, with a Test Double. The simplest, and simultaneously effective, technique for providing a seam is Dependency Injection: make objects ask for dependencies instead of going around to grab them from static sources or global variables.

Implementation

Objects declare a dependency by requiring the corresponding collaborator via a constructor parameter, a setter, or an interface method. The constructor approach is going to cut it in most of greenfield cases, while for refactoring from existing code setters are less invasive..

In the test, we pass in a Test Double instead a collaborator during the arrange phase. This Test Double is an alternate implementation created via subclassing or via an implementation of the required interface (to satisfy type hints, the only compile-time checks of PHP applications). The test focus on the object at hand, since the injected Test Doubles simplify a lot the picture.

In production of course, the real collaborator is injected, in order to form a whole object graph.

Dependency Injection usually simplifies very much the setup phase of the tests, but also improves the production code by cutting assumptions about dependency wiring; the actual objects satisfying a dependency are chosen during the application bootstrap, and the related decisions are not spread in hundreds of constructors.

Although an object featuring Dependency Injection will be more complex to construct, one-time construction responsibility will be kept separated from the responsibilities of the object. This leads to light __construct() methods, which only assign parameters to fields, and to the use of Factories or Dependency Injection containers.

By no means you need an external library like a DI container to implement this pattern: you can perfectly write Factory classes on your own.

The joy of refactoring

If you do not have a seam, when you want to inject a dependency you should definitely introduce an interface and an injection point like a constructor parameter. You may have to revise the contract with the collaborator: it may be too chatty to use a Test Double, calling 6 methods with disparate parameters.

Variations

  • Constructor Injection enforces one-time initialization, and the injected collaborator can never be altered nor the rest of the Api is affected by having to contain new methods for injection. It's the perfect case.
  • Setter Injection is reserved for classes that have many collaborators, a scenario which could also be a smell. Setters are also easy to treat for automated dependency injection, where you do not construct the production version of the object by hand in a factory, but use DI frameworks or some reflection (method_exists()) to save work. For example, you can have a bunch of standard setter that when seen on a certain category of objects cause the injection of standard collaborators.
  • In his xUnit Testing Patterns book, Meszaros proposes a third variation for injection over the classical constructor and setter: Method Parameter Injection. In fact, injection via method parameter is common when the other methods do not require the same collaborator; in those cases, it's not even recognized as Dependency Injection by many TDDers, but just as good object-oriented programming (the only alternative using a singleton.)
  • Fowler instead proposes Interface Injection as the third variation, where the list of setters is formalized in an interface. I don't feel like we strictly need this solution very much in a dynamic language like PHP, although if the interface may be extracted in case it presents itself as a meaningful concept (no DependentOnEntityManagerAndByTheWayAllDoctrine2Interface).

Example

In the code sample injecting a collaborator is compared with inline new() in case the correspondent production class does not exist yet. Testing classes implementing Dependency Injection is closely related to Test Double patterns: Dependency Injection paves the way for them.

<?php
class DependencyInjectionTest extends PHPUnit_Framework_TestCase
{
    /**
     * The System Under Test is exercised in isolation via a .
     */
    public function testInjectsATestDoubleViaConstructor()
    {
        $collaboratorStub = $this->getMock('Collaborator');
        $sut = new GoodSut($collaboratorStub);
        $collaboratorStub->expects($this->any())->method('baseValue')->will($this->returnValue(5));
        $this->assertEquals(50, $sut->calculateValue());
    }

    /**
     * The System Under Test cannot be exercised in isolation, and since
     * the other class is not yet finished we're blocked. We cannot even
     * substitute Collaborator just because of speed if it's a really heavy
     * and complex to set up implementation using a database or the filesystem.
     */
    public function testCannotSubstituteTheCollaborator()
    {
        $sut = new BadSut();
        // now what?
        $this->markTestIncomplete('Kernel panic!');
    }
}

class GoodSut
{
    private $collaborator;

    public function __construct(Collaborator $collaborator)
    {
        $this->collaborator = $collaborator;
    }

    public function calculateValue()
    {
        return $this->collaborator->baseValue() * 10;
    }
}

class BadSut
{
    private $collaborator;

    public function __construct()
    {
        $this->collaborator = new Collaborator();
    }

    public function calculateValue()
    {
        return $this->collaborator->baseValue() * 10;
    }
}

/**
 * The Collaborator class is not even finished yet.
 */
class Collaborator
{
    public function baseValue() {}
}
Dependency injection PHP unit test Object (computer science) Collaborator (software) Test double

Opinions expressed by DZone contributors are their own.

Related

  • Combining gRPC With Guice
  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • The Singleton Design Pattern
  • Micronaut With Relation Database and...Tests

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: