This article will show how to “test drive” the prime factors kata in Smalltalk with SUnit, an xUnit style unit testing framework. In this example, I’m using Pharo so the specifics may be a little bit different if you’re using a different flavour of Smalltalk
For an explanation of Test-Driven Development (TDD) itself, see this article.
Problem: We want to factor a number into it’s base primes. For example 9 can be factored into 3x3 and 20 can be factored into 2x2x5.
The first stage in TDD is red where we create a failing test. It’s important to remember that we don’t want to write any production code until that’s the only way we can make a test pass.
First test: factor 2
For our first test, we’ll attempt to factor 2, which should result in an array containing only two. We create a test class:
1
2
TestCase << #PrimeFactorsTest
package: 'PrimeFactors-Tests'
and define our first test in that. The name has to begin with test, which is how SUnit finds it:
1
2
testFactor2
self assert: (PrimeFactors new factor: 2) equals: #(2)
That accepts without complaint, which is normal. Smalltalk works out which method to run when the message is actually sent, not when the code is compiled, so naming a method that doesn’t exist yet is not an error. This is different from a compiled language where it would be failing to compile at this step. The code is installed and ready to run against a class that isn’t there.
So we run it, and it fails when it tries to send the message factor:.
Here is where Smalltalk stops resembling the other languages in this series, because Smalltalk is not just a language; it’s a full environment. The failure isn’t reported back to us as a line of text. It opens the debugger, stopped at the exact point where the message was sent, with the receiver and the argument live in front of us. And the debugger offers to create the missing method.
There are other environments where the IDE will recognize that a method is missing and will offer to help you create it but what’s happening here is very different. The code ran until it hit that missing method and then the debugger offered to create a new method in already running code, and once it was created, execution continued from that point.
That’s the whole “write enough code to let the test run” step, except we never leave the failure to do it. We create the method from the debugger, land in an empty body, and write it there. Since we want to see it fail first, we create an implementation that won’t pass.
1
2
factor: anInteger
^ #()
Accept, and execution carries on from where it stopped. The test is still failing, only now it’s failing for the right reason.
1
Got #() instead of #(2)
It’s worth being explicit about what just happened, because it’s easy to skip past. In Java we read a compiler error, opened a different file, wrote a stub, and recompiled. Here the failing call was the place we wrote the method, and the program never stopped running while we did it.
Next we move to green, where we need to make the test pass. We’re not looking to make the code beautiful or performant or reusable at this point. We’re looking to make the test pass in the easiest possible way.
1
2
factor: anInteger
^ #(2)
We know that hard coding the 2 at this point is silly and that it’s going to cause problems later but we don’t care. The TDD process will get us there at the right time. For now, we only care about the one case that we’ve written a test for.
Now we come to the third stage in the process, refactor. We look at the code and the tests and we look to see if there’s anything we might want to improve. There usually isn’t for the first test but there certainly will be things to improve once we have a couple of tests and so we always want to explicitly consider this.
Check it in. It’s certainly not complete but what we do have is a tiny slice of working, tested, code.
Second test: factor 3
We already recognized that hard coding the 2 was bad so let’s create another test that forces us to fix that. We’re back in red
We’re adding a second test here, not replacing the first. We are building up a suite of these tests.
1
2
testFactor3
self assert: (primeFactors factor: 3) equals: #(3)
We run the test and it fails for the right reason.
1
Got #(2) instead of #(3)
We satisfied red and now move on to green. Change the production code to make the test pass. The simplest thing that could work.
1
2
factor: anInteger
^ { anInteger }
Now both tests are passing so we consider the refactor step. Is there any duplication? Anything we might want to improve at this point?
There is some duplication in the tests in that they both start the same way. While I might normally wait until I’ve seen duplication three times, I think this one is likely to be in every test that we write so let’s extract that now.
1
2
3
4
5
6
7
8
9
setUp
super setUp.
primeFactors := PrimeFactors new
testFactor2
self assert: (primeFactors factor: 2) equals: #(2)
testFactor3
self assert: (primeFactors factor: 3) equals: #(3)
Run all the tests again and verify we didn’t break anything.
Check it in.
Third test: factor 4 (two times a prime)
red: The test for 4 is straight forward. We should get back two and two.
1
2
testFactor4
self assert: (primeFactors factor: 4) equals: #(2 2)
green: The implementation is more complex here because for the first time, we have a chance to actually calculate something instead of just hard coding.
I notice that any even number can be divided by 2 so check if it’s even and then split into 2 and whatever is left.
1
"factor: dividing out a single 2"
When the tests are run again, we discover that while the test for 4 (the immediate concern) is passing, one of our earlier tests is now failing. Specifically the test for 2.
We’re still in the green phase so we want the simplest solution that gets us working again. We’ll add a special case for 2.
1
"factor: with the special case for 2"
All tests are passing so we move on to refactor. I’m really not liking all the hard coded 2’s but I’m not sure how I want to handle that yet so I’ll defer fixing it. It’s likely now that we’ll work for two times any prime but we may not work for powers of two times a prime. Let’s test-drive that.
Check it in.
Fourth test: factor 8 (power of two times a prime)
red: Create a test for 8 and watch it fail.
1
2
testFactor8
self assert: (primeFactors factor: 8) equals: #(2 2 2)
green: We really just want to execute that condition multiple times, so we need the conditional to become a loop.
1
2
3
4
5
6
7
8
9
factor: anInteger
| remainder result |
remainder := anInteger.
result := OrderedCollection new.
[ (remainder isDivisibleBy: 2) and: [ remainder ~= 2 ] ] whileTrue: [
result add: 2.
remainder := remainder // 2 ].
result add: remainder.
^ result asArray
Now it looks like we’ve handled all powers of two times a prime.
refactor: Back to the refactor step, and I’m increasingly unhappy with all the hard coded 2’s so let’s deal with that now.
1
2
3
4
5
6
7
8
9
10
factor: anInteger
| remainder result divisor |
remainder := anInteger.
result := OrderedCollection new.
divisor := 2.
[ (remainder isDivisibleBy: divisor) and: [ remainder ~= divisor ] ] whileTrue: [
result add: divisor.
remainder := remainder // divisor ].
result add: remainder.
^ result asArray
All we’ve really done is name the 2 so it’s still hard coded. It’s only in one place now, however, which makes the code feel much cleaner to me.
All tests continue to pass so we move on.
Check it in.
Fifth test: factor 9 (power of three times a prime)
red: We know that powers of two are all working and it feels pretty obvious that it’s going to break when we attempt multiples of 3 so let’s start there. Next test is for 3x3
1
2
testFactor9
self assert: (primeFactors factor: 9) equals: #(3 3)
Test fails, and for the right reason.
1
"the failure"
green: For the case of 9, we really want to execute the whole loop for both 2 and 3 so what if we did a loop within a loop?
1
2
3
4
5
6
7
8
9
10
factor: anInteger
| remainder result |
remainder := anInteger.
result := OrderedCollection new.
2 to: 3 do: [ :divisor |
[ (remainder isDivisibleBy: divisor) and: [ remainder ~= divisor ] ] whileTrue: [
result add: divisor.
remainder := remainder // divisor ]].
result add: remainder.
^ result asArray
All tests continue to pass.
refactor: The code and the tests are both looking pretty clean so nothing I want to change. We still always need to stop and reflect at this point though.
Check it in.
Sixth test: factor 25
If we think our way through all the numbers as we go up, we can see that we’ve handled every power of two times a prime and every power of three times a prime but we don’t handle fives.
red: So let’s test for 25.
1
"testFactor25"
green: The simplest possible thing to make the test pass would be to increase the range to 5.
1
2
3
4
5
6
7
8
9
10
factor: anInteger
| remainder result |
remainder := anInteger.
result := OrderedCollection new.
2 to: 5 do: [ :divisor |
[ (remainder isDivisibleBy: divisor) and: [ remainder ~= divisor ] ] whileTrue: [
result add: divisor.
remainder := remainder // divisor ]].
result add: remainder.
^ result asArray
All tests continue to pass. Now I wonder, if we were able to make it work by only changing the range, could I make the range large enough right now to solve for every possible case? We know the divisor can’t be larger than the original number passed in so let’s set the high end of the range to that.
1
2
3
4
5
6
7
8
9
10
factor: anInteger
| remainder result |
remainder := anInteger.
result := OrderedCollection new.
2 to: anInteger do: [ :divisor |
[ (remainder isDivisibleBy: divisor) and: [ remainder ~= divisor ] ] whileTrue: [
result add: divisor.
remainder := remainder // divisor ]].
result add: remainder.
^ result asArray
Everything continues to pass.
At this point, I think we’ve handled every possible case. We might want to spot-check some larger numbers to see if we can uncover something that we didn’t handle, but that’s more exploratory testing and not test-driven development so we’ll stop here.
Check it in.
Recap
We took one step at a time to solve the problem. We created one tiny test and watched it fail (red), we did the simplest thing to make it pass (green), and we looked for opportunities to make it better (refactor). Then we did it again, and again, until we’d finished the feature.
Along the way, we also highlighted a couple of things that make Smalltalk different from other languages.
To see this in a different language, look at these: