Falling in LOVE with Unit Testing

Joe Skeen

Unit testing provides benefits

  • Early bug detection
  • Regression prevention
  • Continuous Integration
  • Refactoring support
  • Better design
  • Better code quality
  • Living Documentation
  • and more!
The more effort I put into testing the product conceptually at the start of the process, the less effort I [have] to put into manually testing the product at the end because fewer bugs ... emerge as a result.

Trish Khoo, Director of Engineering at Octopus Deploy

Unit testing friction

  • Forced to do it
  • Not allowed the time
  • Never learned how to do it well

My Journey

The Art of Unit Testing (not a sponsor)

What is a unit test?

Testing Pyramid
Credit: Moke Cohn, Martin Fowler, and Lawrence Tan

Key 1: Break it down!

a door image by craiyon.com
a privacy doorknob
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob, should also turn the outside knob.
GIVEN the push button is not pressed
WHEN the user turns the inside knob
THEN the outside knob should also turn
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob, should also turn the outside knob.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob, should retract the latch bolt.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob clockwise, should also turn the outside knob counterclockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob counterclockwise, should also turn the outside knob clockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob clockwise, should also turn the inside knob counterclockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob counterclockwise, should also turn the inside knob clockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob, should retract the latch bolt.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob clockwise, should also turn the outside knob counterclockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob counterclockwise, should also turn the outside knob clockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the inside knob, should retract the latch bolt.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob clockwise, should also turn the inside knob counterclockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob counterclockwise, should also turn the inside knob clockwise.
A privacy doorknob, when the push button is not pressed, when the user turns the outside knob, should retract the latch bolt.

Key 2: Care about test code quality

i.e. DRY

  • A privacy doorknob,
    • when the push button is not pressed,
      • when the user turns the inside knob clockwise,
        • should also turn the outside knob counterclockwise.
        • should retract the latch bolt.
      • when the user turns the inside knob counterclockwise,
        • should also turn the outside knob clockwise.
        • should retract the latch bolt.
      • when the user turns the outside knob clockwise,
        • should also turn the inside knob counterclockwise.
        • should retract the latch bolt.
      • when the user turns the outside knob counterclockwise,
        • should also turn the inside knob clockwise.
        • should retract the latch bolt.
  • A privacy doorknob,
    • when the push button is pressed,
      • when the user tries to turn the outside knob clockwise,
        • should not turn the outside knob at all.
        • should not turn the inside knob at all.
        • should not retract the latch bolt.
      • when the user tries to turn the outside knob counterclockwise,
        • should not turn the outside knob at all.
        • should not turn the inside knob at all.
        • should not retract the latch bolt.
      • when the user tries to turn the inside knob clockwise,
        • should pop the push button out.
        • should turn the inside knob clockwise.
        • should turn the outside knob counterclockwise.
        • should retract the latch bolt.
      • when the user tries to turn the inside knob counterclockwise,
        • should pop the push button out.
        • should turn the inside knob counterclockwise.
        • should turn the outside knob clockwise.
        • should retract the latch bolt.

Other use cases

  • A privacy doorknob,
    • when the push button is pressed,
      • when the user tries to close the door (press the latch bolt)
        • should retract the latch bolt.
      • when the user inserts a long pin into the hole on the outside knob,
        • should pop the push button out.

Exceptional use cases

  • A privacy doorknob,
    • when the button is pressed,
      • when the user uses excessive force to try to turn the outside knob,
        • the knob should not break.

Key 3: Focus on what matters

Key 4: Use AAA

  • Arrange: Initialize object, set properties
  • Act: Call the function you are testing
  • Assert: Verify the results

Enough talk, let's see the code!


export class PrivacyDoorknob {
  private _isButtonPressed = false;
  get isButtonPressed(): boolean {
    return this._isButtonPressed;
  }

  pressButton() {
    this._isButtonPressed = true;
  }

  turnOutsideKnob(direction: RotationDirection): IKnobInteractionResult {
    if (this._isButtonPressed) {
      return {
        latchBolt: 'extended',
      };
    }
    return {
      insideKnob: opposite(direction),
      outsideKnob: direction,
      latchBolt: 'retracted',
    };
  }

  turnInsideKnob(direction: RotationDirection): IKnobInteractionResult {
    this._isButtonPressed = false;

    return {
      insideKnob: direction,
      outsideKnob: opposite(direction),
      latchBolt: 'retracted',
    };
  }
}

export type RotationDirection = 'clockwise' | 'counterclockwise';

export interface IKnobInteractionResult {
  outsideKnob?: RotationDirection;
  insideKnob?: RotationDirection;
  latchBolt: 'extended' | 'retracted';
}

function opposite(direction: RotationDirection): RotationDirection {
  switch (direction) {
    case 'clockwise':
      return 'counterclockwise';
    case 'counterclockwise':
      return 'clockwise';
    default:
      throw new Error(`Invalid direction ${direction}`);
  }
}
              
			

describe('', () => {});
			

describe('', () => {
	// * A privacy doorknob,
	//     * when the push button is pressed,
	//         * when the user tries to turn the outside knob clockwise,
	//             * should not turn the outside knob at all.
	//             * should not turn the inside knob at all.
	//             * should not retract the latch bolt.
	//         * when the user tries to turn the outside knob counterclockwise,
	//             * should not turn the outside knob at all.
	//             * should not turn the inside knob at all.
	//             * should not retract the latch bolt.
	//         * when the user tries to turn the inside knob clockwise,
	//             * should pop the push button out.
	//             * should turn the inside knob clockwise.
	//             * should turn the outside knob counterclockwise.
	//             * should retract the latch bolt.
	//         * when the user tries to turn the inside knob counterclockwise,
	//             * should pop the push button out.
	//             * should turn the inside knob counterclockwise.
	//             * should turn the outside knob clockwise.
	//             * should retract the latch bolt.
});
			

describe('A privacy doorknob', () => {
	describe('when the push button is pressed', () => {
		describe('when the user tries to turn the outside knob clockwise', () => {
			it('should not turn the outside knob at all');
			it('should not turn the inside knob at all');
			it('should not retract the latch bolt');
		});
	});
	
	//         * when the user tries to turn the outside knob counterclockwise,
	//             * should not turn the inside knob at all.
	//             * should not retract the latch bolt.
	//         * when the user tries to turn the inside knob clockwise,
	//             * should pop the push button out.
	//             * should turn the inside knob clockwise.
	//             * should turn the outside knob counterclockwise.
	//             * should retract the latch bolt.
	//         * when the user tries to turn the inside knob counterclockwise,
	//             * should pop the push button out.
	//             * should turn the inside knob counterclockwise.
	//             * should turn the outside knob clockwise.
	//             * should retract the latch bolt.
});

			

	
it('should not turn the outside knob at all', () => {
	// Arrange
	const knob = new PrivacyDoorKnob();
	knob.pressButton();

	// Act
	const result = knob.turnOutsideKnob('clockwise');

	// Assert
	expect(result.outsideKnob).not.toBeDefined();
});
it('should not turn the inside knob at all', () => {
	// Arrange
	const knob = new PrivacyDoorKnob();
	knob.pressButton();

	// Act
	const result = knob.turnOutsideKnob('clockwise');

	// Assert
	expect(result.insideKnob).not.toBeDefined();
});
it('should not retract the latch bolt', () => {
	// Arrange
	const knob = new PrivacyDoorKnob();
	knob.pressButton();

	// Act
	const result = knob.turnOutsideKnob('clockwise');

	// Assert
	expect(result.latchBolt).toBe('extended');
});
				

Time to DRY


describe('A privacy doorknob', () => {
	describe('when the push button is pressed', () => {
		describe('when the user tries to turn the outside knob clockwise', () => {
			it('should not turn the outside knob at all', () => {
			// Arrange
			const knob = new PrivacyDoorknob();
			knob.pressButton();

			// Act
			const result = knob.turnOutsideKnob('clockwise');

			// Assert
			expect(result.outsideKnob).not.toBeDefined();
			});
			it('should not turn the inside knob at all', () => {
			// Arrange
			const knob = new PrivacyDoorknob();
			knob.pressButton();

			// Act
			const result = knob.turnOutsideKnob('clockwise');

			// Assert
			expect(result.insideKnob).not.toBeDefined();
			});
			it('should not retract the latch bolt', () => {
			// Arrange
			const knob = new PrivacyDoorknob();
			knob.pressButton();

			// Act
			const result = knob.turnOutsideKnob('clockwise');

			// Assert
			expect(result.latchBolt).toBe('extended');
			});
		});
	});
});
				
			

Don't get carried away

function removeVowels(input: string): string {/* TODO */}

Test:

  • the empty String
  • a small string with some vowels
  • a small string with only vowels
  • a small string with no vowels
  • a very large String
  • a string with complex unicode characters (i.e. emoji)

your coding journey continues

Go forth and unit test!

Contact me:
joe@worldclassengineers.dev

Thanks for watching!