Welcome back to the new issue of “Build Your Own Testing Framework” series! Did you notice, that out testing framework quits on the first failure? It probably should run all tests, collect all failures and present them nicely. This is what we are going to accomplish today:
- Make sure all tests run even when there is a failure.
- Make sure exit code is correct.
This article is the fifth one of the series “Build Your Own Testing Framework” so make sure to stick around for next parts! Find all posts of these series can here.
Shall we get started?
Catch and report a test failure
Our test suite should no longer bubble up any exceptions. We can achieve that by making an appropriate assertion. And also we should verify that other tests execute after the failure:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
As expected, this fails with an appropriate error Error: Expected not to throw error, but thrown 'Expected to be true, but got false'
indicating that we are bubbling up all errors at the moment. Also, notice how the execution of the whole test suite stops at that point, and it just exits the program with error code 1
. A simple try .. catch
block will fix the issue:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
All tests now run successfully. This code is starting to become unreadable, so it is a good point to refactor. We will:
- Extract whole
try .. catch
as a functionrunTest
. Its current responsibility is only to run the test and ignore any failure; - Extract contents of
if
statement that matches the test name as a functionhandleTest
. Its responsibility is to report the test, create a fresh testSuite and kick offrunTest
; - Extract the whole
for
statement asrunAllTests
.
Here is the final snippet of code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Exit with code 1
Now, when at least one test fails in a suite of tests, the whole suite should fail (after running the rest of its tests). And the indicator of such failure should be an exit code of the process. Let’s write a test:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
As you might guess, we will need another object. It will be responsible for interaction with our process, i.e.: something that we can ask to “exit with code 1.” Because we can not ask our process to exit within the test run, we will have to create a spy. And we shall test-drive its functionality. There is something interesting that we should worry about before that - our test suite is passing currently.. but it shouldn’t be!
Let’s step back and think what just happened: clearly, we are writing the test, that can not possibly pass because we do not have ProcessSpy
yet. So we are expecting a failure - we are expecting a thrown exception. That expectation is an important part of test-driven development: at all times we expect a very specific failure or we expect our tests to pass; if we do not receive a failure when expected and receive an unexpected failure, we should stop right there and think which part of our thinking and our assumptions is incorrect.
Right now, tests do not fail, because we are ignoring all exceptions in our try .. catch
that we introduced a couple of minutes ago. If we want to see failures again, let’s modify catch
block to just log all errors it receives:
1 2 3 4 5 6 7 |
|
Now our test suite outputs an expected error: ReferenceError: ProcessSpy is not defined
. Also, it outputs some other failures that happen in our nested runTestSuite
calls - we should fix them by providing silenceFailures
option for nested runTestSuite
call. We can focus now on the ProcessSpy
failure and test-drive it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
I think we have finished test-driving the functionality of ProcessSpy
. It is time to get back to our failing test for a failure resulting in an exit with code 1. When we run this test suite, we are getting the following error message: Error: Expected to equal 1, but got: null
.‘ To pass this test, we will need to store the fact that we had a failure somewhere and at the end of the test suite run we can trigger exit with code 1 or 0, respectively. We could pass around a status
object with boolean property status.failed
and set it to true
in our catch
block:
1 2 3 4 |
|
And at the end of runTestSuite
function we could call process.exit(1)
if status.failed
was true
:
1 2 3 4 5 6 7 |
|
While this works (as in “tests pass after providing fakeProcess
where needed for nested failing runTestSuite
calls”) state changes in this code are starting to be hard to follow and function signatures remind me of some horror movie:
1 2 3 4 |
|
These signatures smell like objects are hiding there in these functions. Let’s find them!
Quest for hidden objects
First, let’s extract the method object from the function runTestSuite
. We will give it a name TestSuiteRunContext
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
Now, if we were to move function runAllTests
inside of this class, we would not need all these arguments (and all other functions we call):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
|
It already looks very nice. The only thing that I do not like about this object yet is that it has stateful properties and stateless properties. I like to have my objects separated by this concern. Let’s extract status
mutable property as a proper TestSuiteRunStatus
object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
I think we have finished the refactoring. Now we should verify that test suite exits with the code 0 when everything passes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Bottom Line
I think we have finished implementing exit code reporting. The code can be found here: https://github.com/waterlink/BuildYourOwnTestingFrameworkPart5
There is still a lot to go through. In a few next episodes we will:
- Report OK and FAIL for each test;
- Output carefully formatted failures to the STDERR;
- Enable our testing framework to run multiple test suite files at once;
- Enable our testing framework to run in a browser (it is javascript after all).
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.