An Introduction to Unit Testing (for WordPress)

A workshop with Thorsten Frommen,
Carl Alexander, and Giuseppe Mazzapica.

Before We Start...

Grab the Code

👉    https://git.io/vpDy1    👈

Either clone the repository...


						git clone [email protected]:tfrommen/wceu-2018-unit-testing.git
					

...or download the ZIP file.

GitHub Repo Header

Install What You Need

For the PHP part, install via Composer:


						composer install
					

For the JavaScript part, install via Yarn or NPM:


						yarn
					
Or:

						npm i
					

And now, let’s get back to the introduction...

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.
  • ...

Ariane-5

  • Inertial navigation software taken from Ariane-4. Untested.
  • All other systems thoroughly tested component-by-component.
  • Ariane-5 had a different trajectory than Ariane-4.
  • Converting 64-bit floating-point data into 16-bit unsigned integer values.
  • There was an exception handler for that. It had been disabled.
  • About 40 seconds after launch, Ariane-5 literally self-destructed. Successfully.

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.

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?';

							static::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 );
						} );
					} );
				

Game Time!

Level 1
Broken Code

Learning Objectives

  • Familiarize with the testing framework.
  • Interpret ouput for failing tests.
  • Debug and fix broken code.
  • Understand the value of testing first-hand.

Running the Tests

For PHP:


						./vendor/bin/phpunit --testsuite exercise1
					

For JavaScript:


						./node_modules/.bin/jest exercise1
					

PHP Exercise

👉 tests/php/exercise1/Exercise1Test.php

PHP Exercise: Output

PHP Exercise 1

PHP Exercise: Test


					public function test_filter_add_error_codes() {

						$error_codes = filter_add_error_code( [] );

						static::assertTrue( in_array( 'workshop', $error_codes ), 'Added "workshop" error code.' );
					}
				

PHP Exercise: Current Code


					function filter_add_error_code( array $error_codes ) {

						$error_codes[] = 'workshops'; // <- This should be "workshop".

						return $error_codes;
					}
				

PHP Exercise: Updated Code


					function filter_add_error_code( array $error_codes ) {

						$error_codes[] = 'workshop';

						return $error_codes;
					}
				

JavaScript Exercise

👉 tests/js/exercise1/LoremIpsum.test.js

JavaScript Exercise: Output

JavaScript Exercise 1

JavaScript Exercise: Test


					describe( 'LoremIpsum', () => {
						test( 'should have expected name', () => {
							const { name } = LoremIpsum;

							expect( name ).toBe( 'unit-testing-workshop/lorem-ipsum' );
						} );
					} );
				

JavaScript Exercise: Current Code


					// ...

					const name = 'unit-testing-workshop/lorem-ipsmu'; // <- This should be "lorem-ipsum".

					// ...

					export default {
						name,
						settings,
					};
				

JavaScript Exercise: Updated Code


					// ...

					const name = 'unit-testing-workshop/lorem-ipsum';

					// ...

					export default {
						name,
						settings,
					};
				

Level 2
Assertions & Expectations

Learning Objectives

  • Familiarize with different types of assertions/matchers.
  • Understand what parts of your code you should create assertions for.
  • Create the missing assertions in the code.

Running the Tests

For PHP:


						./vendor/bin/phpunit --testsuite exercise2
					

For JavaScript:


						./node_modules/.bin/jest exercise2
					

PHP Exercise

👉 tests/php/exercise2/Exercise2Test.php

PHP Exercise: Test


					public function test_disable_plugin_updates_removes_update_plugins_cap() {

						$caps = [
							'update_plugins' => true,
						];

						$caps = filter_disable_plugin_updates( $caps );

						// Add missing assertion here.
					}
				

PHP Exercise: Code


					function filter_disable_plugin_updates( array $allcaps ) {

						unset( $allcaps['install_plugins'] );
						unset( $allcaps['update_plugins'] );

						return $allcaps;
					}
				

PHP Exercise: Updated Test


					public function test_disable_plugin_updates_removes_update_plugins_cap() {

						$caps = [
							'update_plugins' => true,
						];

						$caps = filter_disable_plugin_updates( $caps );

						static::assertArrayNotHasKey( 'update_plugins', $caps );
					}
				

JavaScript Exercise

👉 tests/js/exercise2/array.test.js

JavaScript Exercise: Test


					describe( 'mapObjectsToProperty', () => {
						test( 'should return string as is', () => {
							const data = 'Some data here...';
							const propertyName = 'children';

							const actual = mapObjectsToProperty( data, propertyName );

							// Fill in the expected value.
							expect( actual ).toBe( /* TODO */ );
						} );
					} );
				

JavaScript Exercise: Code


					export function mapObjectsToProperty( data, propertyName ) {
						if ( ! Array.isArray( data ) ) {
							return data;
						}

						return data.map( ( item ) => (
							typeof item === 'object' && propertyName in item
								? item[ propertyName ]
								: item
						) );
					}
				

JavaScript Exercise: Updated Test


					describe( 'mapObjectsToProperty', () => {
						test( 'should return string as is', () => {
							const data = 'Some data here...';
							const propertyName = 'children';

							const actual = mapObjectsToProperty( data, propertyName );

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

Level 3
Mocking Dependencies

Mocking Dependencies to Test in Isolation

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.

Learning Objectives

  • Familiarize with mocking tools.
  • Understand what parts of your code you should create mocks for.
  • Create the missing mocks in the code.

Running the Tests

For PHP:


						./vendor/bin/phpunit --testsuite exercise3
					

For JavaScript:


						./node_modules/.bin/jest exercise3
					

PHP Exercise

👉 tests/php/exercise3/Exercise3Test.php

PHP Exercise: Test


					public function test_delete_member_page_for_no_user_team_member_page() {

						Monkey\Functions\when( 'get_userdata' )->justReturn( true );

						static::assertSame( 1, delete_member_page( 42 ) );
					}
				

PHP Exercise: Code


					function delete_member_page( $user_id ) {

						if ( ! get_userdata( $user_id ) ) {
							return 0;
						}

						$user_page = get_user_team_member_page( $user_id );
						if ( ! $user_page ) {
							return 1;
						}

						if ( ! wp_delete_post( $user_page->ID ) ) {
							return -1;
						}

						return 2;
					}
				

PHP Exercise: Updated Test


					public function test_delete_member_page_for_no_user_team_member_page() {

						Monkey\Functions\when( 'get_userdata' )->justReturn( true );
						Monkey\Functions\when( 'get_user_team_member_page' )->justReturn( null );

						static::assertSame( 1, delete_member_page( 42 ) );
					}
				

JavaScript Exercise

👉 tests/js/exercise3/index.test.js

JavaScript Exercise: Test


					jest.mock( '../../js/src/blocks/LoremIpsum', () => ( {
						name: 'lorem-ipsum',
						settings: { lorem: 'ipsum' },
					} ) );

					describe( 'index', () => {
						test( 'should register LoremIpsum block', () => {
							const { registerBlockType } = global.wp.blocks;

							// Fill in the missing value(s)/variable(s).
							expect( registerBlockType ).toHaveBeenCalledWith( /* TODO, TODO */ );
						} );
					} );
				

JavaScript Exercise: Code


					import LoremIpsum from './blocks/LoremIpsum';
					import Progress from './blocks/Progress';

					const { registerBlockType } = wp.blocks;

					[
						LoremIpsum,
						Progress,
					].forEach( ( { name, settings } ) => {
						registerBlockType( name, settings );
					} );
				

JavaScript Exercise: Updated Test


					jest.mock( '../../js/src/blocks/LoremIpsum', () => ( {
						name: 'lorem-ipsum',
						settings: { lorem: 'ipsum' },
					} ) );

					describe( 'index', () => {
						test( 'should register LoremIpsum block', () => {
							const { registerBlockType } = global.wp.blocks;

							expect( registerBlockType ).toHaveBeenCalledWith( 'lorem-ipsum', { lorem: 'ipsum' } );
						} );
					} );
				

Level 4
Putting It All Together

Learning Objectives

  • Apply all the concepts seen so far.
  • Understand what we want to test based on the test name and description.
  • Create all the assertions and mocks necessary for that test to pass.

Running the Tests

For PHP:


						./vendor/bin/phpunit --testsuite exercise4
					

For JavaScript:


						./node_modules/.bin/jest exercise4
					

PHP Exercise

👉 tests/php/exercise4/Exercise4Test.php

PHP Exercise: Test


					public function test_get_team_page_return_null_if_option_is_not_set() {

					}
				

PHP Exercise: Code


					function get_team_page() {

						$option = get_option( TEAM_PAGE_OPTION, 0 );
						if ( ! $option ) {
							return null; // <- This is what we want to test.
						}

						$post = get_post( $option );
						if ( ! $post ) {
							return null;
						}

						return $post;
					}
				

PHP Exercise: Updated Test


					public function test_get_team_page_return_null_if_option_is_not_set() {

						Monkey\Functions::expect( 'get_option' )
							->with( TEAM_PAGE_OPTION, \Mockery::any() )
							->andReturn( 0 );

						static::assertSame( null, get_team_page() );
					}
				

JavaScript Exercise

👉 tests/js/exercise4/LoremIpsumEdit.test.js

JavaScript Exercise: Test


					describe( '<LoremIpsumEdit />', () => {
						test( 'should ensure at least one paragraph before mounting', () => {

						} );
					} );
				

JavaScript Exercise: Code


					class LoremIpsumEdit extends Component {
						componentWillMount() {
							const { paragraphs } = this.props.attributes;
							if ( ! paragraphs || ! paragraphs.length ) {
								this.updateParagraphs( 1 );
							}
						}

						// ...
					}
				

JavaScript Exercise: Updated Test


					jest.mock( '../../../../js/src/utils/text', () => ( {
						getRandomParagraph: () => 'Random paragraph.',
					} ) );

					describe( '<LoremIpsumEdit />', () => {
						test( 'should ensure at least one paragraph before mounting', () => {
							const setAttributes = jest.fn();
							shallow(
								<LoremIpsumEdit setAttributes={ setAttributes } />
							);

							expect( setAttributes ).toHaveBeenCalledWith( {
								paragraphs: [
									'Random paragraph.',
								],
							} );
						} );
					} );
				

static::assertTrue( did_learn_stuff() );


Questions?