@SirPython has already given us some great steps to improve this implementation but I'd like to focus on the process of getting there as well since that seems to be part of the question.
Is this how one is supposed to solve problems TDD style?
I think it's interesting that you suggest TDD shapes your solution. I find that a test driven approach changes my interfaces but has much less impact on what the final implementation of a function looks like. Whatever process you use, if you end up with "very messy" code then I think something needs to change. Code is written for humans to read and messy code will cause you grief in the future.
I'm not sure what your workflow looked like to arrive at this solution but here's what I would expect:
- Write a test describing what the unit you are testing should do.
- See the test fail.
- Write code to pass the test.
- See the test pass.
- Consider if a refactor could improve your implementation and if so apply it.
- See that all the tests still pass, giving you confidence that your refactor was a safe change.
You have a bunch of test cases but running through and verifying them manually seems tedious. Let's start by making that easy. There are a bunch of testing libraries you might use but we can practice TDD with something simple:
function runTests(tests) {
var failureCount = 0
var testsCount = tests.length
var testTimerName = 'tests completed in'
console.time(testTimerName)
for (var t = 0; t < testsCount; t++) {
var test = tests[t]
var input = test['input']
var expectation = test['expected']
var output = convert(input)
if (output != expectation) {
console.log('Failure: expected convert(' + input + ') to equal \'' + expectation + '\' but got \'' + output + '\'')
failureCount += 1
}
}
console.log(testsCount - failureCount + '/' + testsCount + ' tests passed.')
console.timeEnd(testTimerName)
}
var tests = [
{'input': 5, 'expected': 'V'},
]
runTests(tests)
When run we get:
ReferenceError: convert is not defined
We need to at least implement a convert
function:
function convert(num) {
return ''
}
>
Failure: expected convert(5) to equal 'V' but got ''
0/1 tests passed.
tests completed in: 1ms
A test failed. That's good, we know we'll see failures when they happen. Let's make the test pass.
function convert(num) {
return 'V'
}
>
1/1 tests passed.
tests completed in: 1ms
Now we have a quick way to run our tests every time we make a change and see which, if any, failed. That should allow us to make changes to convert
fearlessly, confident that we will know if we're improving the implementation or if we break behaviors which used to work.
From here we could add one test case at a time, update convert
to make the new test pass, clean up our work, and then repeat. Sometimes that sort of incremental approach works great. Alternately we might write a bunch of tests, all of which will fail for now, and then work on getting more and more of them to pass. Since we already have some idea what our implementation might look like let's take that second approach.
var tests = [
{'input': 1, 'expected': 'I'},
{'input': 2, 'expected': 'II'},
{'input': 3, 'expected': 'III'},
{'input': 4, 'expected': 'IV'},
{'input': 5, 'expected': 'V'},
{'input': 9, 'expected': 'IX'},
{'input': 12, 'expected': 'XII'},
{'input': 16, 'expected': 'XVI'},
{'input': 29, 'expected': 'XXIX'},
{'input': 44, 'expected': 'XLIV'},
{'input': 45, 'expected': 'XLV'},
{'input': 68, 'expected': 'LXVIII'},
{'input': 83, 'expected': 'LXXXIII'},
{'input': 97, 'expected': 'XCVII'},
{'input': 99, 'expected': 'XCIX'},
{'input': 500, 'expected': 'D'},
{'input': 501, 'expected': 'DI'},
{'input': 649, 'expected': 'DCXLIX'},
{'input': 798, 'expected': 'DCCXCVIII'},
{'input': 891, 'expected': 'DCCCXCI'},
{'input': 1000, 'expected': 'M'},
{'input': 1004, 'expected': 'MIV'},
{'input': 1006, 'expected': 'MVI'},
{'input': 1023, 'expected': 'MXXIII'},
{'input': 2014, 'expected': 'MMXIV'},
{'input': 3999, 'expected': 'MMMCMXCIX'},
]
runTests(tests)
>
Failure: expected convert(2) to equal 'II' but got 'I'
Failure: expected convert(3) to equal 'III' but got 'II'
Failure: expected convert(4) to equal 'IV' but got 'II'
23/26 tests passed.
tests completed in: 3ms
Interesting, looks like there are at least a few cases this implementation does not handle correctly yet. We can fix these and running our tests as we go will make sure we don't break other test cases (like 'IX'
) as we make changes.
In addition to helping us implement new behavior these tests are also useful for the final and essential "refactor" step in a TDD "red-green-refactor" workflow. Now that we have test verifying that this function behaves as we expect we can step back and consider how the implementation can be improved with confidence that we haven't changed its behavior, as long as our tests still pass. If we apply the changes @SirPython suggested we will hopefully see that our tests still pass and perhaps even run in less time.
Taking time for this final refactor step is, I think, essential for making sure incrementally developed behaviors do not end up as the sort of messy but probably valid implementation @Mast wanted to avoid.