Skip to content

Latest commit

 

History

History
496 lines (355 loc) · 16.8 KB

factorial-tdd.md

File metadata and controls

496 lines (355 loc) · 16.8 KB

Factorial TDD, with JavaScript and Jest

The Problem

We want to write some code that will calculate a factorial given a number as an input.

What is a factorial?

Multiplying all whole numbers from our given number down to 1. Factorials are indicated by a "!" sign and they are whole numbers.

Whole Numbers are positive integers and 0. For example; 0, 1, 2, 3, and so on.

You can calculate the factorial of a number by the following formula. x! = x * (x-1)!

Which is equal to x! = x * (x-1) * (x-2) * … * 3 * 2 * 1

Also 1! = 1 and 0! = 1 keep this in mind.

For example: 4! = 4 * 3 * 2 * 1 which is 24 in other words 4! = 4 * (4 - 1) * (4 - 2) * (4 - 3)

Lets' implement factorial with TDD!

I will be very verbose, and try to mimic actual development iterations and steps. The code will not be optimized until we are done. Then we will do the refactoring, which I will also show.

Let's start!

Do not think about the code! Think about the use cases and tests

Do not think about the code! You still are! :)

Think about one and only one example, one simple use case, a simple happy path. Think it's result, what should it be. Write the test for it than the code.


  • Given x = 0, then the factorial should be 1

    1. Write the test, run the test. Test fails... See /test/factorial.01.test.js We should not even create the file for the actual code at this stage.

      test('0! should be 1', () => {
      	expect(factorial(0)).toBe(1);
      });
    2. Now we write the code, run the test (fix the code until the test passes) See /src/factorial.01.js

      if (0 === parameter) {
      	return 1;
      }

  • Repeat for Given x = 1, then the factorial should be 1

    1. Write the test, run the test. Test fails... See /test/factorial.02.test.js

      test('1! should be 1', () => {
      	expect(factorial(1)).toBe(1);
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.02.js

      if (1 === parameter) {
      	return 1;
      }

  • Repeat for Given x = 5, then the factorial should be 120 (5 * 4 * 3 * 2 * 1)

    1. Write the test, run the test. Test fails... See /test/factorial.03.test.js

      test('5! should be 120', () => {
      	expect(factorial(5)).toBe(120);
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.03.js

      if (parameter > 1) {
      	return parameter * factorial(parameter - 1);
      }

      I used recursion heree, but a loop will do the same job. Feel free to use whatever best suites you.

How about the exceptions, negative use cases?

  • Repeat for Given x = -1, should throw an exception. (negative number)

    1. Write the test, run the test. Test fails... See /test/factorial.04.test.js Please note: when checking if the method throws an exception, it has to be wrapped into a function, since JEST cannot accept methods with parameters, that throws an exception. (We will refactor this in the future steps as well.)

      test('Negative parameter should throw an exception.', () => {
      	function wrapper() {
      		factorial(-5);
      	}
      	expect(wrapper).toThrow('-5 is a negative number.');
      });		
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.04.js

      if (parameter < 0) {
      	throw new Error(parameter + ' is a negative number.');
      }

  • Repeat for Given x = 2.3, should throw an exception. (floating number)
    1. Write the test, run the test. Test fails... See /test/factorial.05.test.js

      test('Decimal parameter should throw an exception.', () => {
      	function wrapper() {
      		factorial(2.3);
      	}
      	expect(wrapper).toThrow('2.3 is a decimal number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.05.js

      if (parameter !== Math.trunc(parameter)) {
      	throw new Error(parameter + ' is a decimal number.');
      }

  • Repeat for Given x = “x”, should throw an exception. (string)
    1. Write the test, run the test. Test fails... See /test/factorial.06.test.js

      test('String parameter should throw an exception.', () => {
      	function wrapper() {
      		factorial('x');
      	}
      	expect(wrapper).toThrow('x is not a number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.06.js

      if ('string' === typeof parameter) {
      	throw new Error(parameter + ' is not a number.');
      }

  • Repeat for Given x = true, should throw an exception (boolean)
    1. Write the test, run the test. Test fails... See /test/factorial.07.test.js

      test('Boolean parameter should throw an exception.', () => {
      	function wrapper() {
      		factorial(true);
      	}
      	expect(wrapper).toThrow('true is not a number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.07.js

      if ('boolean' === typeof parameter) {
      	throw new Error(parameter + ' is not a number.');
      }

  • Repeat for Given x = undefined, should throw an exception (undefined)
    1. Write the test, run the test. Test fails... See /test/factorial.08.test.js

      test('Undefined parameter should throw an exception.', () => {
      	function wrapper() {
      		let x;
      		factorial(x);
      	}
      	expect(wrapper).toThrow('undefined is not a number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.08.js

      if (undefined === parameter) {
      	throw new Error(parameter + ' is not a number.');
      }

  • Repeat for Given x = null, should throw an exception (null)
    1. Write the test, run the test. Test fails... See /test/factorial.09.test.js

      test('Null parameter should throw an exception.', () => {
      	function wrapper() {
      		let x = null;
      		factorial(x);
      	}
      	expect(wrapper).toThrow('null is not a number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.09.js

      if (null === parameter) {
      	throw new Error(parameter + ' is not a number.');
      }

  • Repeat for Given x = {chapter: 1}, should throw an exception (object)
    1. Write the test, run the test. Test fails... See /test/factorial.10.test.js

      test('Object parameter should throw an exception.', () => {
      		function wrapper() {
      			let x = {chapter: 1};
      			factorial(x);
      		}
      		expect(wrapper).toThrow('[object Object] is not a number.');
      });
    2. Write the code, run the test (fix the code until the test passes) See /src/factorial.10.js

      if ('object' === typeof parameter) {
      	throw new Error(parameter + ' is not a number.');
      }

  • Repeat for Given x is not passed , should throw an exception (no parameter passed)
    1. Write the test, run the test. Tests do pass. See /test/factorial.11.test.js. It turns out that not passing a parameter and passing an uninitialized variable as parameter throw the same exception. They are both undefined.

      No Parameter Undefined Variable
      factorial(); let x; factorial(x);
      test('No parameter should throw an exception.', () => {
      	function wrapper() {
      		factorial();
      	}
      	expect(wrapper).toThrow('undefined is not a number.');
      });
    2. We do not need to write any additional code, but we do need to start thinking about making our code better, reorganizing, refactoring. See /src/factorial.11.js (No change compare to factorial.10.js.)


We might add some more tests but the ones we have so far covered almost all the use cases the happy path and the unhappy path.

Refactor and optimize your code.

At this point we can have the confidence to easily change our code since have our unit tests. We can makes mistakes, and our test will catch it. If we miss anything we can, and we should add a test for it.


  • Instead of checking if parameter is type like a "string", "boolean", "object", "undefined' or "null" we can check if it is a number. Can we replace all below code?

     if ('object' === typeof parameter) {
     	throw new Error(parameter + ' is not a number.');
     }
     
     if (null === parameter) {
     	throw new Error(parameter + ' is not a number.');
     }
     
     if (undefined === parameter) {
     	throw new Error(parameter + ' is not a number.');
     }
     
     if ('boolean' === typeof parameter) {
     	throw new Error(parameter + ' is not a number.');
     }
     
     if ('string' === typeof parameter) {
     	throw new Error(parameter + ' is not a number.');
     }

    With just one conditional, one if statement?

     if ('number' !== typeof parameter) {
     	throw new Error(parameter + ' is not a number.');
     }

    We change our code, and run our tests. Everything works! Great, we reduced lines in our code. The less code the better. Please see. /test/factorial.12.test.js and /src/factorial.12.js


  • “FUNCTIONS SHOULD DO ONE THING. THEY SHOULD DO IT WELL. THEY SHOULD DO IT ONLY.” 1. We will use the extract method refactoring pattern 2 in this enhancement. In our code we are checking if our parameter is a valid whole number. That code should go to its own method and we should call it from the factorial. We are creating a dependency here but we will manage that as well in this example.

    We should extract the following logic into its own method. We can update our code to check if the parameter is a whole number return true else return false.

     if ('number' !== typeof parameter) {
     	throw new Error(parameter + ' is not a number.');
     }
     
     if (parameter < 0) {
     	throw new Error(parameter + ' is a negative number.');
     }
     
     if (parameter !== Math.trunc(parameter)) {
     	throw new Error(parameter + ' is a decimal number.');
     }

    into

     function isWholeNumber(parameter) {
     
     	if ('number' !== typeof parameter) {
     		return false;
     	}
     	
     	if (parameter < 0) {
     		return false;
     	}
     	
     	if (parameter !== Math.trunc(parameter)) {
     		return false;
     	}
     	
     	return true;
     }

    Now our factorial code is down to quite a few lines, great! But when we run our test they fail. We need to update the exception text in our tests to "is not a whole number". Now all our test do pass. Please see /test/factorial.12.test.js and /src/factorial.12.js.


  • From the last example we still have several problems. First changing the exception, error text in multiple places. We need do something about this. Second, isWholeNumber is a private internal function. It is not possible to test it explicitly we need to make it a part of a utility class. We need to create a math utility class and make both factorial and isWholeNumber methods of it.

    1. First let refactor our implementation and test file names to MathUtil, do not change the actual code except the reference to the file import. Please see /test/MathUtil.01.test.js and /src/MathUtil.01.js. Run our test all pass.

    2. Let us create MathUtil class and make factorial and isWholeNumber it's methods. Run tests, they fail, update reference to function factorial to MathUtil.factorial method. Run tests, they pass. Please see /test/MathUtil.02.test.js and /src/MathUtil.02.js.

      class MathUtil {
      
      	/**
      	* Check if a number is a whole number. In other words, a number that is 0 or a positive integer.
      	* @param parameter {number}
      	* @returns {boolean}
      	*/
      	static isWholeNumber(parameter) {
      	
      		if ('number' !== typeof parameter) {
      			return false;
      		}
      		
      		if (parameter < 0) {
      			return false;
      		}
      		
      		if (parameter !== Math.trunc(parameter)) {
      			return false;
      		}
      		
      		return true;
      	}
      	
      	/**
      	* Calculates of a given whole number.
      	* @param parameter {number} Whole number.
      	* @returns {number}
      	* @throws {Error} Not a whole number.
      	*/
      	static factorial(parameter) {
      	
      		if (false === MathUtil.isWholeNumber(parameter)) {
      			throw new Error(parameter + ' is not a whole number.');
      		}
      		
      		if (0 === parameter) {
      			return 1;
      		}
      		
      		if (1 === parameter) {
      			return 1;
      		}
      		
      		if (parameter > 1) {
      			return parameter * MathUtil.factorial(parameter - 1);
      		}
      	}
      }
    3. Let us externalize the hard coded exception text. This refactoring pattern is called Replace Magic Literal 3. We add the static messages to the MathUtil class and update all the references both in MathUtil and MathUtil test. Please see /test/MathUtil.03.test.js and /src/MathUtil.03.js.

      static messages = {
      	NOT_A_WHOLE_NUMBER: ' is not a whole number.'
      }

      The references will look like below. Keep in mind MathUtil is a static class.

      expect(wrapper).toThrow('-5' + MathUtil.messages.NOT_A_WHOLE_NUMBER);

  • We also should convert our wrapper functions to arrow functions. The code will look way more cleaner and smaller. We will not change the implementation code, only the tests. Please see /test/MathUtil.04.test.js and /src/MathUtil.04.js.

     expect( () => MathUtil.factorial(-5)).toThrow('-5' + MathUtil.messages.NOT_A_WHOLE_NUMBER);

    Also the last conditional in our MathUtil.factorial is not necessary since we cover hopefully all the cases. A well written method by default always must return a value if there is no exception thrown.

     // parameter > 1
     return parameter * MathUtil.factorial(parameter - 1);

  • Lastly we need to split the tests for MathUtil.isWholeNumber and MathUtil.factorial into separate test suites. Please keep in mind redundancy in testing is a good thing, so all the tests that apply to whole numbers will be copied into whole number suite. Please see /test/MathUtil.test.js and /src/MathUtil.js for final versions.

Code Coverage

What is code coverage? Testing all the lines of code (loc or in other words statements), all the branches in the conditionals and all the loops. The result is in percentage.

One important point; 100% code coverage does not guarantee there will be no errors in your code. You might forget to add an edge case, an alternate happy path. So when the code fails, first thing you have to do is to write the test for it and then implement the fix. This is typical TDD workflow.

We did not get 100% code coverage until the last changes. There are several reasons.

  1. If you are using a conditional and a return from within; there must be a default return of a function; which we did not have in our initial iterations. When you first write your test and code, this is fine. Until you finish the entire functionality, do not try to fix code coverage. Since you do TDD you should be very close to 100% anyway. You will see why do you not hit 100% and refactor your code, you might find some very interesting behavior like we will talk next.

  2. There is a very interesting historic behavior of null. Even it is a primitive JavaScript type, the typeof null returns an object. Unfortunately this not fixed for backward compatibility. So our factorial.10.js and factorial.11.js did not cover all the branches.

    if ('object' === typeof parameter) {
    	throw new Error(parameter + ' is not a number.');
    }
    
    if (null === parameter) {
    	throw new Error(parameter + ' is not a number.');
    }

    The second conditional was never hit in our tests, since 'object' === typeof parameter short circuited when we test null parameter.

Final thoughts

Like Edward Demming once said, Inspection is too late. The quality, good or bad, is already build-in the product. Which without unit tests it is very hard if not impossible to talk about code quality.

Unit tests are the heart and soul of software development. I would highly recommend xUnit Test Patterns: Refactoring Test Code, by Gerard Meszaros if you want learn the testing design patterns. Test code will get messy and just like any code needs affection, time and refactoring. Finally we are all software engineers are indebted to Kent Beck for TDD and yes "Good code matters!".


  1. Clean Code: A Handbook of Agile Software Craftsmanship, by Bob "Uncle" Martin -- Chapter 3, Function

  2. Refactoring: Improving the Design of Existing Code, 2nd Edition, by Martin Fowler. -- Extract Functions.

  3. Refactoring: Improving the Design of Existing Code, 2nd Edition, by Martin Fowler. -- Replace Magic Literal.