How (Not) to
Write Testable Code

A talk by @thorstenfrommen

What is Testable Code?

Every Piece of Software is Testable.
Somehow.

What you might be able to test:

  • return value;
  • output;
  • other side effects;
  • doesn’t crash;
  • ...

Testable Code
in the Context of
Unit Testing.

Unit Testing

Also component testing, or module testing.

Unit Testing

  • Dynamic testing of individual units. In isolation.
  • Find defects in software components (e.g., functions, classes).
  • Done by developers.

Dynamic Testing

  • Execution of code.
  • Primarily verification.
  • Find failures.
  • Different testing methods:
    • black-box testing;
    • white-box testing;
    • ...

Verification

The process of evaluating a system or component to determine whether the system of a given development phase satisfies the conditions imposed at the start of that phase. IEEE Std 610.12-1990

“Are we building the product right?„

White-box Testing

  • Structure-based testing.
  • Close examination of procedural level of detail.
  • Knowledge of internals required.
  • Different white-box testing techniques:
    • statement testing;
    • decision testing;
    • (multiple) condition testing;
    • ...

Unit Test Examples

Example 1: Description

Example 1: Subject under Test (SUT)


class Oracle {

    public function get_answer() {

        return '42';
    }
}

Example 1: Test


class OracleTest extends PHPUnit_Framework_TestCase {

    public function test_get_answer() {

        $testee = new Oracle();

        $this->assertSame( '42', $testee->get_answer() );
    }
}

Example 1: JavaScript Version


var test = require( 'tape' );

var Oracle = require( './path/to/oracle' );

test( function( assert ) {
    var testee = new Oracle();

    assert.equal( testee.getAnswer(), '42' );

    assert.end();
} );

Example 2: Description

  • A class with a conditional method.
  • An external dependency.
  • Thus (maybe) use a mocking framework:

Example 2: SUT


class FortuneTeller {

    private $oracle;

    public function __construct( Oracle $oracle ) {

        $this->oracle = $oracle;
    }

    public function get_answer( $money = 0 ) {

        if ( (int) $money < 5 ) {
            return '';
        }

        return $this->oracle->get_answer();
    }
}

Example 2: Test


class FortuneTellerTest extends PHPUnit_Framework_TestCase {

    public function test_get_answer() {

        $oracle = $this->getMockBuilder( 'Oracle' )
            ->getMock();

        $oracle->method( 'get_answer' )
            ->willReturn( 'answer' );

        $testee = new FortuneTeller( $oracle );

        $this->assertSame( '', $testee->get_answer( 0 ) );
        $this->assertSame( 'answer', $testee->get_answer( 5 ) );
        // ...use a data provider for this test.
    }
}

Example 2: Mockery Version


class FortuneTellerMockeryTest extends PHPUnit_Framework_TestCase {

    public function test_get_answer() {

        $oracle = Mockery::mock( 'Oracle', [
            'get_answer' => 'answer',
        ] );

        $testee = new FortuneTeller( $oracle );

        $this->assertSame( '', $testee->get_answer( 0 ) );
        $this->assertSame( 'answer', $testee->get_answer( 5 ) );
        // ...use a data provider for this test.
    }
}

Example 2: JavaScript Version


var test = require( 'tape' );
var sinon = require( 'sinon' );

var FortuneTeller = require( './path/to/fortune-teller' );

test( function( assert ) {
    var testee = new FortuneTeller( {
        getAnswer: sinon.stub().returns( 'answer' )
    } );

    assert.equal( testee.getAnswer( 0 ), '' );
    assert.equal( testee.getAnswer( 5 ), 'answer' );
    // ...

    assert.end();
} );

Example 3: Description

  • Similar to example 1.
  • Fires a WordPress action hook.
  • Provides a WordPress filter hook.
  • Thus use a WordPress mocking framework (PHP):

Example 3: SUT


class HookOracle {

    public function get_answer() {

        do_action( 'give_answer' );

        return (string) apply_filters( 'the_answer', '42' );
    }
}

Example 3: Test


class HookOracleBrainMonkeyTest extends PHPUnit_Framework_TestCase {

    // Set up and tear down Brain Monkey...

    public function test_get_answer() {

        $testee = new HookOracle();

        $this->assertSame( '42', $testee->get_answer() );
        $this->assertSame( 1, did_action( 'give_answer' ) );
        $this->assertTrue( Brain\Monkey::filters()->applied( 'the_answer' ) > 0 );
    }
}

Example 3: WP_Mock Version


class HookOracleWP_MockTest extends PHPUnit_Framework_TestCase {

    // Set up and tear down WP_Mock...

    public function test_get_answer() {

        $testee = new HookOracle();

        WP_Mock::expectAction( 'give_answer' );
        WP_Mock::onFilter( 'the_answer' )
            ->with( '42' )
            ->reply( '4815162342' );
        $this->assertSame( '4815162342', $testee->get_answer() );
    }
}

Bad Practices
and How to
Make Them Good

Issue: No Feedback


function maybeDoSomething() {
    if ( ! checkSomething() ) {
        return;
    }

    doSomething();
}

Solution: Give Feedback


function maybeDoSomething() {
    if ( ! checkSomething() ) {
        return false;
    }

    doSomething();

    return true;
}

But consider potential side effects (e.g., JavaScript event listeners).

Issue: Terminating the Execution


class Checker {

    public function check_data( $data ) {

        if ( ! $data ) {
            exit();
        }

        // ...
    }
}

Solution: Delegation


class Checker {

    public function check_data( $data ) {

        if ( ! $data ) {
            $this->call_exit();
        }

        // ...
    }

    protected function call_exit() {

        exit();
    }
}

Issue: Leaving the Context


function processData( data ) {
    data = prepareData( data );
    sendData( data );

    window.location.href = 'http://example.com';
}

Solution: Delegation


function setLocation( url ) {
    window.location.href = url;
}

function processData( data ) {
    data = prepareData( data );
    sendData( data );

    setLocation( 'http://example.com' );
}

Issue: Creating (Real) Objects


class Renderer {

    public function render_formatted_data( $data ) {

        $formatter = new Formatter();
        echo $formatter->format( $data );
    }
}

Solution: Dependency Injection

Here, constructor injection:


class Renderer {

    private $formatter;

    public function __construct( Formatter $formatter ) {

        $this->formatter = $formatter;
    }

    public function render_formatted_data( $data ) {

        echo $this->formatter->format( $data );
    }
}

“What if I cannot work with an injected object,
but need to create it dynamically?„

Solution: Use a Factory


class Renderer {

    private $formatter_factory;

    public function __construct( FormatterFactory $formatter_factory ) {

        $this->formatter_factory = $formatter_factory;
    }

    public function render_formatted_data( $data, $options ) {

        $formatter = $this->formatter_factory->create( $options );
        echo $formatter->format( $data );
    }
}

Issue: Global State


function someFunction() {
    if ( 42 === globalFoo ) {
        // ...
    }

    // ...
}

Solution: Inject What You Need


function someFunction( localFoo ) {
    if ( 42 === localFoo ) {
        // ...
    }

    // ...
}

Issue: Using Unresettable Singletons


class Singleton {

    private static $instance;

    // protected/private __construct() and __clone(), ...

    // ...and other code, which maybe manipulates the object's state...

    public static function get_instance() {

        if ( ! self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }
}

Solution: Add a reset() Method


class Singleton {

    // ...

    public static function get_instance() {

        if ( ! self::$instance ) {
            self::$instance = new self();
        }

        return self::$instance;
    }

    public static function reset() {

        self::$instance = null;
    }
}

Issue: Breaking the Law of Demeter


function isIdEven( someObject ) {
    var id = someObject.getOtherObject().getId();

    return ! ( id % 2 );
}

Excursus: Law of Demeter

A method m of an object o should invoke only the methods of:

  • o itself;
  • any objects passed to m;
  • any objects created/instantiated in m;
  • any properties/members of o.

Issue: Breaking the Law of Demeter


function isIdEven( someObject ) {
    var id = someObject.getOtherObject().getId();

    return ! ( id % 2 );
}

function filterByEvenId( someObject ) {
    if ( isIdEven( someObject ) ) {
        return someObject;
    }

    return undefined;
}

Solution: Don’t Talk to Strangers


function isIdEven( otherObject ) {
    var id = otherObject().getId();

    return ! ( id % 2 );
}

function filterByEvenId( someObject ) {
    if ( isIdEven( someObject.getOtherObject() ) ) {
        return someObject;
    }

    return undefined;
}

Issue: Calling Static Methods


class Renderer {

    public function render_if_okay( $data ) {

        if ( ThirdPartyChecker::is_okay( $data ) ) {
            echo $data;
        }
    }
}

Solution: Wrap Static Method


class Checker {

    public function is_okay( $data ) {

        return ThirdPartyChecker::is_okay( $data );
    }
}

Solution: Wrap Static Method


class Renderer {

    private $checker;

    public function __construct( Checker $checker ) {

        $this->checker = $checker;
    }

    public function render_if_okay( $data ) {

        if ( $this->checker->is_okay( $data ) ) {
            echo $data;
        }
    }
}

Issue: Incomplete Initialization


function Renderer() {
    var formatter;

    this.setFormatter = function( f ) {
        formatter = f;
    };

    this.getFormattedData = function( data ) { // <-- SUT
        return formatter.format( data );
    };
}

Solution: Use Constructor Injection


function Renderer( formatter ) {
    this.getFormattedData = function( data ) {
        return formatter.format( data );
    };
}

Issue: Creating Intermediary Objects


function render_post_title_with_id( $post_id ) {

    $post = get_post( $post_id );
    if ( $post ) {
        echo $post->post_title . '(' . $post_id . ')';
    }
}

Solution: Inject What You Need


function render_post_title_with_id( WP_Post $post ) {

    echo $post->post_title . '(' . $post->ID . ')';
}

Issue: eval Expressions


function badlyDesignedFunction( data ) {
    return eval( '42 === data' );
}

Solution: Write Regular Code


function okayDesignedFunction( data ) {
    return 42 === data;
}

Issue: Insane Complexity


class Processor {

    public function process( $data ) {

        // 16 lines full of different checks, a lot of if—else, ...

        // 23 lines full of data preparing, switch—case all over, ...

        // 42 lines full of data processing...

        // 8 lines full of post-processing, if—elseif—else again...
    }
}

Solution: Write Modular Code


class Processor {

    public function process( $data ) {

        if ( ! $this->checker->check( $data ) ) {
            return false;
        }

        $data = $this->prepare( $data );

        // 42 lines full of data processing...

        $this->post_processor->process( $data );

        return true;
    }
}

Lessons Learned

  • Unit testing means testing in isolation.
  • Don’t create your dependencies, inject them instead.
  • Keep away from global state/scope.
  • Write modular code, and split up complex stuff.
  • Write testable code—even if you don’t test it yourself.

References and Further Reading

Thanks!

@thorstenfrommen

slides.tfrommen.de/testable-code

github.com/tfrommen/testable-code