Welcome back to the new issue of “Build Your Own Testing Framework” series! Today we are going to unit-test runTestSuite
function of our testing framework. Currently, only its happy path is implicitly tested via every test of the system. Additionally, this function’s unhappy paths are, in fact, untestable at the moment.
This article is the third one of the series “Build Your Own Testing Framework”, so make sure to stick around for next parts! All articles of these series can be found here.
Shall we get started?
Testing existing code
Let’s take another look at the runTestSuite
function:
1 2 3 4 5 6 7 8 9 |
|
This function currently:
- Creates a new test suite from the passed in function-constructor.
- Finds every method that starts with the string
test
. - And calls every such method.
Test that it calls at least one test method
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
And this test passes. To be sure, that we are actually testing anything, let’s make sure that testSomeInterestingFunction
is not being called:
1 2 3 4 5 |
|
This fails as expected: Error: Expected to be true, but got false
. Undoing this mutation causes the test to pass again. This is good, since we have seen this test fail when we expect it to fail. This proves the semantic stability of our test.
So, what will happen if we replace the whole if
condition with true
?
1 2 3 |
|
As expected, all tests will pass. Seems that we need to add a new test here:
Test that it does not call non-test methods
1 2 3 4 5 6 7 8 9 10 11 |
|
And this fails as expected. This makes our test suite semantically stable against this sort of mutation. Undoing the mutation should make the test suite pass. And it does.
There is another surviving mutant that I can come up with:
1 2 3 4 5 6 |
|
This means, that only first function will only ever run. Since all our tests are currently verifying that only one function is called or not we will need another test to defeat this mutant:
Test that it calls all provided test methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
Careful here: testItCallsAllTestMethods
has to be the first test in the test suite for it to be ever called with the current mutation. As expected this test fails and undoing the mutation makes it pass.
testItCallsAllTestMethods
is superior to the testItCallsOneTestMethod
, so we can remove the latter.
The amount of duplication in this code does not make me happy. Seems like we are missing the ability to verify if a certain function was called or not. Let’s try to extract this abstraction:
1 2 3 4 5 6 7 8 9 |
|
And then the usage would look like that:
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 |
|
Testing assertions.spy()
It seems, that having t.spy()
available for the users of our testing framework might be very useful! Let’s 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 |
|
Test testItCanBeCalledAsFunction
is inferior to testIsCalledAfterBeingCalled
, so we can remove it.
To make the assertion more fluent we might want to have aSpy.assertCalled()
and aSpy.assertNotCalled()
:
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 |
|
Let’s do the same with the other test:
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 |
|
And the full implementation of spy()
function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Bottom Line
Today we have tested all the existing behavior of the runTestSuite
function. That has driven us to implement very simple spies for our testing framework.
We have successfully applied manual Mutational Testing to the existing functionality to derive Semantically Stable tests for it. We did some Triangulation too today. Generally speaking, in TDD Triangulation technique is something that is used on a daily basis, when TDD is practiced properly. Future articles will expand on Triangulation, Mutational Testing, and Semantic Stability in more detail, so stay tuned!
There are only a few problems left, that bother me:
- We often do
t.assertTrue(!condition)
, seems that we lackt.assertFalse(condition)
assertion. - We often call functions without any assertions to do an implicit assertion, that the call is not throwing any exception. This can be confusing: it is better to make that assertion explicitly - seems like we need
t.assertNotThrow
. - Seems that it is useful to have
NOT
version of every assertion. Even though we don’t needt.assertNotEqual
right now, from my experience with testing it is often very useful.
Creating these assertions I will leave as an exercise to the reader. From now on, we will assume they are implemented and we will use them where appropriate. Code is available on GitHub: https://github.com/waterlink/BuildYourOwnTestingFrameworkPart3
Next time we will add more requirements for our runTestSuite
function, such as:
- Continue running tests after the first failure.
- Report successfully passed tests.
- Report failures.
- Report test run stats (counts of successful and failed tests).
- Avoid shared state between tests of the same test suite.
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.