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 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
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
Partner Zones AWS Cloud
by AWS Developer Relations
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
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Software Design and Architecture
  3. Integration
  4. Practical PHP Testing Patterns: Test Hook

Practical PHP Testing Patterns: Test Hook

Giorgio Sironi user avatar by
Giorgio Sironi
·
May. 26, 11 · Interview
Like (0)
Save
Tweet
Share
802 Views

Join the DZone community and get the full member experience.

Join For Free

The Test Hook pattern is, again, an option for replacing dependencies at runtime. It is a very lightweight, and as so easy to use on legacy code: it does not require you to inject anything in the production code.

The pattern consists in modifying the SUT itself to remove the dependency, substitute it, or anyway simplyfing the test scenario. But the production code does not change its external behavior. This pattern is a measure of last resort: it allows you to deal with hardcoded class names, filesystem calls or to interrupt spooky actions-at-a-distance.

Implementation

This pattern is implemented by subclassing: one or more method overrides modify a part of the SUT. The targets may be a Factory Method or the hooks of Template Methods.

The production code can continue to use their own hardcoding for now, all we need is a little refactoring to encapsulate the creation of the collaborator in a single place that we can override. If we have to override a long method, we'll also have to rewrite part of it.

For substitution of Test Doubles, the target is strictly a Factory Method, but it's just a special case: the original implementation may not be related to creation, but just to grabbing the collaborator from some source (such as a Singleton or Registry). There are other options than substitute a Factory Method, similar to the Test-Specific Subclass approach: for example replacing a crucial method which calls a static class or a Singleton to simplify the test scenario.

A case for refactoring

Once you have got the tests running, why refactor away from a Test Hook? Indeed the introduction of Dependency Injection may take a while, as you have to look up all the places where the SUT is created and make sure the collaborators are passed to it. This kind of large-scale refactoring applies well to a whole application, when you start defining the lifecycle of your objects instead of letting them be defined locally by whoever calls new().

I think a good rule of thumb for extracting a collaborator instead of a Test Hook is seeing the possibility for substitution of the collaborator with another implementation, applying a naive form of the Strategy pattern: in our code sample, it would be displaying date and times extracted from some source instead of the current one. Replacing a collaborator is simpler than creating yet another subclass, and will pay you back for the time you take for the refactoring.

Example

In the code sample, a Test Hook is introduced into an HTML box displaying the current date. Since we do not want to adjust our assertion every day, the job of the hook is to provide a fixed date.
<?php

/**
 * The TestCase shows how first only a brittle test and a loose assertion
 * can be made over the result. With proper isolation, the assertion becomes 
 * an equality: at the same time TestableTimeBox isn't going to break client
 * code where UntestableTimeBox was used, because we only changed an internal
 * detail and not the public Api (like its constructor).
 */
class TestHookTest extends PHPUnit_Framework_TestCase
{
    public function testTheUntestableTimeBoxWithASmokeTest()
    {
        $box = new UntestableTimeBox();
        $this->assertRegexp('/<div(.*)<\/div>/', $box->__toString());
    }

    public function testTheTestableTimeBoxWithAUnitTest()
    {
        $box = new TestSubclassOfTestableTimeBox(0000001);
        $this->assertEquals('<div class="current_timestamp">1970-01-01</div>', $box->__toString());
    }

    public function testTheTestableTimeBoxWithAUnitTestAndAPartialGeneratedMock()
    {
        $box = $this->getMock('TestableTimeBox', array('currentTime'));
        $box->expects($this->any())
            ->method('currentTime')
            ->will($this->returnValue(0000001));
        $this->assertEquals('<div class="current_timestamp">1970-01-01</div>', $box->__toString());
    }
}

/**
 * The original SUT: there is no way to fully test it due to the global state
 * introduced by time().
 */
class UntestableTimeBox
{
    public function __toString()
    {
        return '<div class="current_timestamp">' . date('Y-m-d', time()) . '</div>';
    }
}

/**
 * The SUT with a really small refactoring, which does not break its Api.
 * Of course in reality it would have the same name of the original SUT...
 */
class TestableTimeBox
{
    public function __toString()
    {
        return '<div class="current_timestamp">' . date('Y-m-d', $this->currentTime()) . '</div>';
    }

    protected function currentTime()
    {
        return time();
    }
}

/**
 * A Test-Specific Subclass that overrides the Test Hook. This is *not* part 
 * of production code.
 * You add all these lines of code specific just in order to run a simple test:
 * the payback you get is the shorter test length. A next step could be to 
 * inject a simple collaborator wrapping time().
 */
class TestSubclassOfTestableTimeBox extends TestableTimeBox
{
    public function __construct($currentTime)
    {
        $this->currentTime = $currentTime;
    }

    protected function currentTime()
    {
        return $this->currentTime;
    }
}
Testing Hook PHP

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • gRPC on the Client Side
  • Top 10 Best Practices for Web Application Testing
  • Important Data Structures and Algorithms for Data Engineers
  • [DZone Survey] Share Your Expertise and Take our 2023 Web, Mobile, and Low-Code Apps Survey

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

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: