An Introduction to Unit Testing (for WordPress)


A talk by @thorstenfrommen

Who’s That Guy?

Thorsten Frommen

Thorsten Frommen

  • Certified PHP engineer, web development professional, and tester.
  • Senior WordPress Engineer at Human Made.
  • Co-organizer of the local WordPress meetup in Aachen, Germany.

What Is Testing?

Testing

The process consisting of all life cycle activities, both static and dynamic, concerned with planning, preparation and evaluation of software products and related work products to determine that they satisfy specified requirements, to demonstrate that they are fit for purpose and to detect defects. ISTQB®

This Is a Test

There are five differences. 🧐

Why Test?

Reasons for Testing

  • Software contains defects. Th🐞y h🐛de.
  • Defects can cause software failures.
  • Failures can cost money, and even be mortal!
  • Confidence in the code.
  • Software quality assurance.
  • Accelerated software development.
  • ...

Testable Code

Every Piece of Software Is Testable

There are several things you might be able to test:

  • the return value of a function;
  • the output of a function;
  • other side effects of executing a function;
  • whether or not a program crashes;
  • ...

Testable Code
in the Context of
Unit Testing

Unit Testing

Also component testing or module testing.

What Is Unit Testing?

Where Is Unit Testing?

The “Common” V-model

The common V-model

Unit Testing Is
Dynamic Testing of Individual Units in Isolation

Unit Testing Is Dynamic Testing

Dynamic Testing

  • Execution of code.
  • Potential defects manifest as failures.
  • These failures will get debugged.
  • Individual bugs will get fixed.
  • Consequence: improved software quality.

Validation

The process of evaluating a system or component during or at the end of the development process to determine whether it satisfies specified requirements. IEEE Std 610.12-1990

“Are we building the right product?”

White-box Testing

Unit testing is applying white-box testing techniques,
meaning close examination of procedural level of detail.

Designing tests requires full knowledge of internals.

Subject Under Test


					function get_answer(): string {

						return '42';
					}
				

To test if a function behaves as expected, one has to know what to expect.

Defining Units

From Characters to Classes

Possible units in code:

  • characters as atomic level;
  • words (e.g., values, keywords, and names);
  • expressions (or statements);
  • logic (e.g., conditional, and mathematical);
  • functions wrap expressions and logic;
  • classes (or modules) encapsulate functions (and state);
  • ...

Reasonable Units

Logic is testable, and so are functions and classes/modules.

Going even higher to namespaces, packages and so on is no good.

Define a unit as either logic, or function, or class/module.

Revisiting Logic


					function doubleOrHalf( number ) {
						if ( number > 0 ) {
							return number * 2;
						}

						return number / 2;
					}
				

Logic very often not testable as individual unit.

A unit is either a function, or a class/module.

Testing in Isolation

Important! Often overlooked.

Testing in Isolation

or

Knowing the Reason for a Failing Unit Test

Unit Test...?


					function test_register_taxonomy() {

						$tax = rand_str();

						$this->assertFalse( taxonomy_exists( $tax ) );

						register_taxonomy( $tax, 'post' );

						$this->assertTrue( taxonomy_exists( $tax ) );
						$this->assertFalse( is_taxonomy_hierarchical( $tax ) );

						unset( $GLOBALS['wp_taxonomies'][ $tax ] );
					}
				

This is not a unit test. But it has every right to exist.

Unit Test Example 1

Example 1: Subject under Test (SUT)


					function get_answer(): string {

						return '42';
					}
				

					export function getAnswer() {
						return '42';
					}
				

Example 1: Inspection

Example 1: PHP Test


					class FunctionsTest extends \PHPUnit\Framework\TestCase {

						public function test_get_answer_return_expected_answer() {

							$expected = '42';

							$actual = get_answer();

							$this->assertSame( $expected, $actual );
						}
					}
				

Example 1: JavaScript Test


					describe( 'getAnswer()', () => {
						test( 'should return expected answer', () => {
							const expected = '42';
							const actual = getAnswer();

							expect( actual ).toBe( expected );
						} );
					} );
				

Unit Test Example 2

Example 2: SUT


					class Oracle {

						public function answer(): string {

							return '42';
						}
					}
				

					class Oracle {
						answer() {
							return '42';
						}
					}

					export default Oracle;
				

Example 2: Inspection

  • A stupid simple class method.
  • No external dependencies.
  • Use a (unit) testing framework only.

Example 2: PHP Test


					class OracleTest extends \PHPUnit\Framework\TestCase {

						public function test_return_expected_answer() {

							$this->assertSame( '42', ( new Oracle() )->answer() );
						}
					}
				

Example 2: JavaScript Test


					describe( 'Oracle', () => {
						test( 'should return expected answer', () => {
							expect( ( new Oracle() ).answer() ).toBe( '42' );
						} );
					} );
				

Unit Test Example 3

Example 3: PHP Code


					class FortuneTeller {

						private $oracle;

						public function __construct( Oracle $oracle ) {

							$this->oracle = $oracle;
						}

						public function answer( int $money = 0 ): string {

							return ( $money >= 5 ) ? $this->oracle->answer() : '';
						}
					}
				

Example 3: JavaScript Code


					class FortuneTeller {
						constructor( oracle ) {
							this.oracle = oracle;
						}

						answer( money = 0 ) {
							return ( Number( money ) >= 5 ) ? this.oracle.answer() : '';
						}
					}

					export default FortuneTeller;
				

Example 3: Inspection

  • A class with a conditional method, taking some input.
  • An external dependency...?!

🙀

How to Test in Isolation?

Mocking Dependencies

Define dummy functions and objects that you have absolute control over.


Take care of two things:

  • no errors when executing method under test;
  • all dependencies are fake.

Example 3: Inspection

  • A class with a conditional method, taking some input.
  • An external dependency.
  • Maybe use a mocking library (for PHP):

Example 3: PHP Test


					class FortuneTellerTest extends \PHPUnit\Framework\TestCase {

						use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

						public function test_return_expected_answer() {

							$answer = 'some answer here';

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

							$fortune_teller = new FortuneTeller( $oracle );

							$this->assertSame( '', $fortune_teller->answer( 0 ) );
							$this->assertSame( $answer, $fortune_teller->answer( 100 ) );
						}
					}
				

Example 3: PHPUnit Data Provider


					class FortuneTellerTest extends \PHPUnit\Framework\TestCase {

						use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;

						/** @dataProvider provide_answer_data */
						public function test_return_expected_answer( string $expected, int $money, string $answer ) {

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

							$this->assertSame( $expected, ( new FortuneTeller( $oracle ) )->answer( $money ) );
						}

						public function provide_answer_data() { /* ... */ }
					}
				

Example 3: PHPUnit Data Provider


					class FortuneTellerTest extends \PHPUnit\Framework\TestCase {

						// ...

						public function provide_answer_data() {

							return [
								'not enough money' => [
									'expected' => '',
									'money'    => 0,
									'answer'   => 'some answer here',
								],
								'enough money'     => [
									'expected' => 'some answer here',
									'money'    => 100,
									'answer'   => 'some answer here',
								],
								// ...
							];
						}
					}
				

Example 3: JavaScript Test


					describe( 'FortuneTeller', () => {
						test( 'should return expected answer', () => {
							const answer = 'some answer here';

							const oracle = {
								answer: () => answer,
							};

							const fortuneTeller = new FortuneTeller( oracle );

							expect( fortuneTeller.answer( 0 ) ).toBe( '' );
							expect( fortuneTeller.answer( 100 ) ).toBe( answer );
						} );
					} );
				

Example 3: Parameterized JavaScript Test


					import each from 'jest-each';

					describe( 'FortuneTeller', () => {
						const answer = 'some answer here';
						const oracle = {
							answer: () => answer,
						};

						each( [
							[ '', 0 ],
							[ '', '' ],
							[ answer, 5 ],
							[ answer, 100 ],
							// ...
						] ).test( 'should return expected answer', ( expected, money ) => {
							const actual = ( new FortuneTeller( oracle ) ).answer( money );

							expect( actual ).toBe( expected );
						} );
					} );
				

Unit Test Example 4

Example 4: PHP Code


					class TranslatingOracle {

						public function answer(): string {

							return __( 'The answer is 42.', 'some-textdomain-here' );
						}
					}
				

Example 4: JavaScript Code


					class TranslatingOracle {
						answer() {
							return wp.i18n.__( 'The answer is 42.', 'some-textdomain-here' );
						}
					}

					export default TranslatingOracle;
				

Example 4: Inspection

Example 4: PHP Test


					class TranslatingOracleTest extends \PHPUnit\Framework\TestCase {

						// Tear down Brain Monkey...

						public function test_return_number() {

							$answer = 'translated answer here';

							\Brain\Monkey\Functions\expect( '__' )
								->once()
								->with( \Mockery::type( 'string' ), 'some-textdomain-here' )
								->andReturn( $answer );

							$this->assertSame( $answer, ( new TranslatingOracle() )->answer() );
						}
					}
				

Example 4: JavaScript Test


					describe( 'TranslatingOracle', () => {
						test( 'should return expected answer', () => {
							const translation = 'some translation here';
							global.wp = {
								i18n: {
									__: jest.fn( () => translation ),
								},
							};

							expect( ( new TranslatingOracle() ).answer() ).toBe( translation );

							expect( global.wp.i18n.__ ).toHaveBeenCalledTimes( 1 );
							expect( global.wp.i18n.__ ).toHaveBeenCalledWith( 'The answer is 42.', 'some-textdomain-here' );

							delete global.wp;
						} );
					} );
				

Unit Test Example 5

Example 5: PHP Code


					class HookAwareOracle {

						public function answer(): string {

							do_action( 'give_answer' );

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

Example 5: Inspection

  • Similar to example 2.
  • Fires an action.
  • Provides a filter.
  • Use a WordPress mocking library:

Example 5: PHP Test


					class HookAwareOracleTest extends \PHPUnit\Framework\TestCase {

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

						public function test_return_unfiltered_answer() {

							$this->assertSame( '42', ( new HookAwareOracle() )->answer() );

							$this->assertSame( 1, did_action( 'give_answer' ) );

							$this->assertSame( 1, \Brain\Monkey\Filters\applied( 'the_answer' ) );
						}
					}
				

Unit Test Example 6

Example 6: JavaScript Code


					import React from 'react';

					const Answer = ( props ) => {
						const { id, text } = props;

						return (
							<li className="oracle__answer">
								Answer { id }: <em>{ text }</em>
							</li>
						);
					};

					export default Answer;
				

Example 6: Inspection

Example 6: JavaScript Test


					import React from 'react';
					import TestRenderer from 'react-test-renderer';

					describe( 'Answer', () => {
						test( 'should render an answer according to the passed props', () => {
							const id = 'some-id-here';
							const text = 'Some text here.';
							const testRenderer = TestRenderer.create(
								<Answer id={ id } text={ text } />
							);

							const output = testRenderer.toJSON();

							expect( output.type ).toBe( 'li' );
							expect( output.props.className ).toContain( 'oracle__answer' );
							expect( output.children ).toContain( id );

							expect( output ).toMatchSnapshot();
						} );
					} );
				

Example 6: Snapshot File


					exports[`<Answer /> should render an answer according to the passed props 1`] = `
					<li
						className="oracle__answer"
					>
					  Answer
					  some-id-here
					  :
					  <em>
					    Some text here.
					  </em>
					</li>
					`;
				

Unit Test Example 7

Example 7: JavaScript Code


					import React, { Component } from 'react';

					import Answer from './Answer';

					class ReactOracle extends Component {
						render() {
							const { answers } = this.props;

							return (
								<ul className="oracle">
									{ answers.map( ( answer ) => (
										<Answer key={ answer.id } { ...answer } />
									) ) }
								</ul>
							);
						}
					}

					export default ReactOracle;
				

Example 7: Inspection

  • JSX.
  • Props.
  • Other components (i.e., dependencies).
  • Use a React testing library.

Example 7: JavaScript Test


					import React from 'react';
					import { shallow } from 'enzyme';

					describe( '<ReactOracle />', () => {
						test( 'should render list of answers passed props', () => {
							const answers = [
								{ id: 23, text: 'Illuminatus!' },
								{ id: 42, text: '42.' },
							];
							const wrapper = shallow(
								<Testee answers={ answers } />
							);

							expect( wrapper.is( 'ul.oracle' ) );

							answers.forEach( ( { id, text } ) => {
								expect( wrapper.containsMatchingElement( <Answer id={ id } text={ text } /> ) );
							} );
						} );
					} );
				

Writing Good Unit Tests

Every Unit Test Should Answer Five Questions

  1. What are you testing?
  2. What should it do?
  3. What is the expected result (i.e., output, and/or return value)?
  4. What is the actual result?
  5. How can the test be reproduced?

Unit Test Template: PHP


					class WhatClassTest extends \PHPUnit\Framework\TestCase {

						public function test_what_method_should_do_what() {

							$expected = 'What is the expected result?';

							$actual = 'What is the actual result?';

							$this->assertSame( $expected, $actual, 'What should it do? (optional)' );
						}
					}
				

Unit Test Template: JavaScript


					describe( 'What are you testing?', () => {
						test( 'What should it do?', () => {
							const expected = 'What is the expected result?';
							const actual = 'What is the actual result?';

							expect( actual ).toBe( expected );
						} );
					} );
				

Key Take-aways

Unit testing...

  • ...aims at finding defects;
  • ...involves execution of code;
  • ...means testing of individual units in isolation;
  • ...requires full knowledge of application internals;
  • ...determines if the software satisfies the requirements.

References and Further Reading

Thank You!


@thorstenfrommen

slides.tfrommen.de/unit-testing

github.com/tfrommen/unit-test-examples