Today we are going to unit-test existing functionality of our own testing framework, that we have test-driven with FizzBuzzKata in the previous part.
This needs to be done, since currently only happy paths are implicitly tested via FizzBuzzKata test suite, i.e.: when all the tests pass. The following unhappy paths are not tested at the moment:
when assertTrue fails, it throws an error,
when assertEqual fails, it throws an error,
when the test fails, it renders the error and fails the whole test suite run, i.e.: non-zero exit code.
Let’s check if that test actually is testing anything by trying to break the implementation of assertTrue:
1234567
// src/TestingFramework.jsassertTrue:function(condition,message){if(condition){// <- this condition was invertedthrownewError(...);}},
And if we run it, we get the expected error:
123456789
/usr/local/bin/node AssertTrueTest.js/path/to/project/src/TestingFramework.js:4thrownewError(message||"Expected to be true, but got false");^Error:Expectedtobetrue,butgotfalse..stacktrace..Processfinishedwithexitcode1
OK, it fails as expected, now we should undo our breaking change in the implementation and run the test suite again and it should pass:
1234567
// src/TestingFramework.jsassertTrue:function(condition,message){if(!condition){// <- the breaking change here have been undonethrownewError(...);}},
Now, let’s write a new test for the case, when assertTrue fails:
123456789
// test/AssertTrueTest.jsthis.testFailure=function(){try{t.assertTrue(false);}catch(error){t.assertEqual("Expected to be true, but got false",error.message);}};
And if we run the test suite, it passes. If I was confident in the previous test, that I know why it didn’t fail, here I feel a bit uncomfortable about writing these 5 lines of test code and never seeing them fail. So let’s break the code once again!
1234567
// src/TestingFramework.jsassertTrue:function(condition,message){if(!condition){thrownewError(message||"oops");// <- we have changed error message here}},
And if we undo our breaking change in the implementation the test should pass:
12345678
// src/TestingFramework.jsassertTrue:function(condition,message){if(!condition){thrownewError(message||"Expected to be true, but got false");// ^ we have restored correct message ^}},
And the last test for assertTrue to test the custom failure message:
123456789
// test/AssertTrueTest.jsthis.testCustomFailureMessage=function(){try{t.assertTrue(false,"it is not true!");}catch(error){t.assertEqual("it is not true!",error.message);}};
Even that I am confident enough, that this try { ... } catch (..) { ... } construction does the right thing, let’s be diligent about it and break the implementation of that functionality and see this test fail:
12345678
// src/TestingFramework.jsassertTrue:function(condition,message){if(!condition){thrownewError("Expected to be true, but got false");// ^ 'message || ' was removed here ^}},
If we run the test suite:
123456789
/usr/local/bin/node AssertTrueTest.js/path/to/project/src/TestingFramework.js:4thrownewError("Expected to be true, but got false");^Error:Expectedtobetrue,butgotfalse..stacktrace..Processfinishedwithexitcode1
It fails, but this change breaks assertEqual too so that we don’t see any meaningful error message now. We can figure out if that is an expected failure or not by inspecting the stack trace:
If we open the code at this stack trace frame, we will see:
1
t.assertEqual("it is not true!",error.message);
Great, this is exactly what we expected. Undoing the breaking change and running the test suite again:
12345678
// src/TestingFramework.jsassertTrue:function(condition,message){if(!condition){thrownewError(message||"Expected to be true, but got false");// ^ 'message || ' was inserted here again ^}},
Interestingly enough, I know how to break the code now and all the tests will pass:
First, let’s extract local variable in assertTrue:
123456789
// src/TestingFramework.jsassertTrue:function(condition,message){varerrorMessage=message||"Expected to be true, but got false";if(!condition){thrownewError(errorMessage);}},
This is rather interesting. It seems, that we will have to make sure, that message parameter actually gets passed to new Error(...). We can do that by applying the Triangulation technique focusing on message parameter:
123456789
// test/AssertTrueTest.jsthis.testCustomFailureMessage_withOtherMessage=function(){try{t.assertTrue(false,"should be true");}catch(error){t.assertEqual("should be true",error.message);}};
And if we run the test suite, we get our expected failure:
I think we have all required tests for assertTrue now. This is great! And have you spotted the duplication already?
Refactoring AssertTrueTest
The duplication that is present here is our try { ... } catch (..) { ... } construct. Let’s make it a completely duplicate code by extracting couple of local variables:
1234567891011121314151617
// first, let's extract the Action Under the Test variable:varaction=function(){t.assertTrue(false);};try{action();}catch(error){t.assertEqual("Expected to be true, but got false",error.message);}// next, let's extract the Expected Error Message:varexpectedMessage="Expected to be true, but got false";try{action();}catch(error){t.assertEqual(expectedMessage,error.message);}
If we apply the same refactoring for all tests in the AssertTrueTest suite, we will see the duplication:
Use this function in all tests and inline all the extracted local variables:
1234567891011121314151617
this.testFailure=function(){assertThrow("Expected to be true, but got false",function(){t.assertTrue(false);});};this.testCustomFailureMessage=function(){assertThrow("it is not true!",function(){t.assertTrue(false,"it is not true!");});};this.testCustomFailureMessage_withOtherMessage=function(){assertThrow("should be true",function(){t.assertTrue(false,"should be true");});};
Function assertThrow seems to be useful for any test, not just AssertTrueTest suite. Let’s move it to the assertions object. We will be doing that by using Parallel Change technique:
Create new functionality first;
Step by step migrate all calls to use new functionality instead of old one;
Once old functionality is not used, remove it.
The advantage of that method is that it consists of very small steps, that can be executed with confidence and each such small step never leaves the user in a red state (failing tests or compile/parsing errors).
Let’s see how this one can be applied here:
1. Create new functionality first - we can do it by copying the assertThrow function to assertions object:
12345678910111213
// src/TestingFramework.jsvarassertions={// ...assertThrow:function(expectedMessage,action){try{action();}catch(error){// `t` needs to be changed to `this` herethis.assertEqual(expectedMessage,error.message);}}};
2. Step by step migrate all calls to use new functionality instead of old one - we do it by calling assertThrow on t (our assertions object) in the test suite. Since we still haven’t removed the old assertThrow function, we can do that one function call at the time and the tests will always be green:
123456789101112131415161718192021222324
this.testFailure=function(){t.assertThrow("Expected to be true, but got false",function(){// ^ 't.' added here ^t.assertTrue(false);});};// run tests and they still pass.this.testCustomFailureMessage=function(){t.assertThrow("it is not true!",function(){// ^ 't.' added here ^t.assertTrue(false,"it is not true!");});};// run tests and they still pass.this.testCustomFailureMessage_withOtherMessage=function(){t.assertThrow("should be true",function(){// ^ 't.' added here ^t.assertTrue(false,"should be true");});};
3. Once old functionality is not used, remove it. - now we can remove our assertThrow function defined inside of the AssertTrueTest suite and run tests:
And they pass. Let’s see the complete AssertTrueTest suite again:
1234567891011121314151617181920212223242526
// test/AssertTrueTest.jsvarrunTestSuite=require("../src/TestingFramework");runTestSuite(function(t){this.testSuccess=function(){t.assertTrue(true);};this.testFailure=function(){t.assertThrow("Expected to be true, but got false",function(){t.assertTrue(false);});};this.testCustomFailureMessage=function(){t.assertThrow("it is not true!",function(){t.assertTrue(false,"it is not true!");});};this.testCustomFailureMessage_withOtherMessage=function(){t.assertThrow("should be true",function(){t.assertTrue(false,"should be true");});};});
The only problem here is that we are relying on the untested assertThrow assertion here. Let’s unit-test it.
Testing assertThrow(expectedMessage, action)
Let’s create a new test suite with first test when assertThrow succeeds:
The code can be broken without test failure by not using expectedMessage parameter:
12345678910
// src/TestingFramework.jsassertThrow:function(expectedMessage,action){try{action();}catch(error){this.assertEqual("an error message",error.message);// ^ here 'expectedMessage' was changed to constant ^}}
Let’s triangulate the code to make sure expectedMessage is used correctly by adding a new test:
1234567
// test/AssertThrowTest.jsthis.testSuccess_withDifferentExpectedMessage=function(){t.assertThrow("a different error message",function(){thrownewError("a different error message");});};
Next test is to make sure, that assertThrow is actually comparing actual and expected error messages correctly:
123456789
// test/AssertThrowTest.jsthis.testFailure=function(){t.assertThrow("Expected to equal an error message, but got: a different error message",function(){t.assertThrow("an error message",function(){thrownewError("a different error message");});});};
And it passes. The last test, that assertThrow needs is the case, when action() is not throwing any error. In that case assertThrow should fail:
123456789
// test/AssertThrowTest.jsthis.testFailure_whenActionDoesNotThrow=function(){t.assertThrow("Expected to throw an error, but nothing was thrown",function(){t.assertThrow("an error message",function(){// does nothing});});};
Oh, and that test is passing! We clearly don’t have that functionality yet. We have to add another test without usage of outer t.assertThrow to make sure that we get a test failure:
12345678910111213
this.testThrows_whenActionDoesNotThrow=function(){varhasThrown=false;try{t.assertThrow("an error message",function(){// does nothing});}catch(error){hasThrown=true;}t.assertTrue(hasThrown,"it should have thrown");};
And if we run our tests we get the expected failure:
We can fix that by verifying, that catch block is executed in the assertThrow function:
1234567891011121314
// src/TestingFramework.jsassertThrow:function(expectedMessage,action){varhasThrown=false;// <- we initialize hasThrown heretry{action();}catch(error){hasThrown=true;// <- and we set it to true in the catch blockthis.assertEqual(expectedMessage,error.message);}this.assertTrue(hasThrown);// <- and we check that it is true}
And now if we run the test suite, the original test that we were trying to write fails as expected:
// test/AssertThrowTest.jsvarrunTestSuite=require("../src/TestingFramework");runTestSuite(function(t){this.testSuccess=function(){t.assertThrow("an error message",function(){thrownewError("an error message");});};this.testSuccess_withDifferentExpectedMessage=function(){t.assertThrow("a different error message",function(){thrownewError("a different error message");});};this.testFailure=function(){t.assertThrow("Expected to equal an error message, but got: a different error message",function(){t.assertThrow("an error message",function(){thrownewError("a different error message");});});};this.testFailure_whenActionDoesNotThrow=function(){t.assertThrow("Expected to throw an error, but nothing was thrown",function(){t.assertThrow("an error message",function(){// does nothing});});};this.testThrows_whenActionDoesNotThrow=function(){varhasThrown=false;try{t.assertThrow("an error message",function(){// does nothing});}catch(error){hasThrown=true;}t.assertTrue(hasThrown,"it should have thrown");};});
Last one for today is assertEqual:
Testing assertEqual(expected, actual)
Let’s create a test suite with the first test, when assertEqual succeeds:
This test passes, and it can be broken without test failure by always comparing to 42:
12345678
// src/TestingFramework.jsassertEqual:function(expected,actual){this.assertTrue(42==actual,// <- here 'expected' is replaced with constant"Expected to equal "+expected+", but got: "+actual);},
Triangulation to fix that:
1234567891011
// testthis.testSuccess_whenExpectedIsDifferent=function(){t.assertEqual(29,29);};// Error: Expected to equal 29, but got: 29// fix implementation:expected==actual,// <- here 'expected' is restored// and the test passes
Next mutation that does not break any tests looks this way:
123456789
// src/TestingFramework.jsassertEqual:function(expected,actual){this.assertTrue(expected==actual,// "Expected to equal " + expected + ", but got: " + actual"oops"// <- replace error message);},
Let’s add the test to protect from this kind of mutation:
12345
this.testFailure=function(){t.assertThrow("Expected to equal 42, but got: 29",function(){t.assertEqual(42,29);});};
This fails with Error: oops, because assertThrow uses assertEqual. Stack trace shows, that the failure is happening here:
12
// src/TestingFramework.js in assertThrowthis.assertEqual(expectedMessage,error.message);
So that is the expected failure. We can fix it by always providing the message "Expected to equal 42, but got: 29":
12345678
// src/TestingFramework.js in assertionsassertEqual:function(expected,actual){this.assertTrue(expected==actual,"Expected to equal 42, but got: 29"// ^ here the exact constant is used ^);},
This needs some more triangulation:
12345678910111213
this.testFailure_withDifferentExpectedAndActual=function(){t.assertThrow("Expected to equal 94, but got: 1027",function(){t.assertEqual(94,1027);});};// Error: Expected to equal 42, but got: 29// fix:this.assertTrue(expected==actual,"Expected to equal "+expected+", but got: "+actual);
I think we are done now with testing the assertEqual function. The AssertEqualTest suite is looking like that now:
123456789101112131415161718192021222324
// test/AssertEqualTest.jsvarrunTestSuite=require("../src/TestingFramework");runTestSuite(function(t){this.testSuccess=function(){t.assertEqual(42,42);};this.testSuccess_whenExpectedIsDifferent=function(){t.assertEqual(29,29);};this.testFailure=function(){t.assertThrow("Expected to equal 42, but got: 29",function(){t.assertEqual(42,29);});};this.testFailure_withDifferentExpectedAndActual=function(){t.assertThrow("Expected to equal 94, but got: 1027",function(){t.assertEqual(94,1027);});};});
// src/TestingFramework.jsvarassertions={assertTrue:function(condition,message){varerrorMessage="Expected to be true, but got false";if(message){errorMessage=message;}if(!condition){thrownewError(errorMessage);}},assertEqual:function(expected,actual){this.assertTrue(expected==actual,"Expected to equal "+expected+", but got: "+actual);},assertThrow:function(expectedMessage,action){varhasThrown=false;try{action();}catch(error){hasThrown=true;this.assertEqual(expectedMessage,error.message);}this.assertTrue(hasThrown,"Expected to throw an error, but nothing was thrown");}};
Bottom Line
Congratulations! We have completely unit-tested our assertions assertTrue and assertEqual. This resulted in natural emergence of the new assertion - assertThrow. We have unit-tested it too!
Additionally, we have practiced usage of Mutational Testing and Triangulation Technique to detect missing test cases and derive them from the code. Also, we have slightly touched the Parallel Change refactoring technique - we will see more of that in the future in these series.
Now that we have unit-tested some basic assertions, we should unit-test our testing framework test runner: runTestSuite function. This will be covered in next series of “Build Your Own Testing Framework”. Stay tuned!
Thanks!
Thank you for reading, my dear reader. If you liked it, please share this article on social networks and follow me on twitter: @tdd_fellow.
If you have any questions or feedback for me, don’t hesitate to reach me out on Twitter: @tdd_fellow.