Ok, lets give this a try. We are going to use TCR (Test + Commit || Revert) to solve the String Calculator Kata. This is an integer calculator that takes in a string with a formula and returns its result in a string. So it could be “1” or “1+3”, or “2-4”. We will stick with integers and multiplication and subtraction for now. Maybe we will make it more sophisticated later.
So first we need to make our first test:
import io.kotest.matchers.string.shouldBeEmpty
import kotlin.test.Test
class KataStringCalculatorTest() {
@Test
fun emptyStringShouldReturnEmptyString() {
KataStringCalculator("").calc().shouldBeEmpty()
}
}This of course fails. Why do I put the input into the calculator and not into the calc routine? I believe the calculator can be an instance. Therefore it can take the setup for the calculator, which is the formula. This is a design decision that it will be relatively easy to change later. You may have a different feeling and therefore end up with a different set of software.
So we know that if we committed now, it would undo our test and everything, and we would have to start over. Not to worry though—we don’t commit failing tests usually anyway, unless we need a large guiding test. So we would need to figure out how to get it to only revert when already accepted tests or unit tests fail - sounding complicated. Instead of the “Growing Object Oriented Software, Guided By Tests” methodology, we could use the Todo List methodology where we design our feature in a todo list and then one by one knock the pieces off the list until we can put the whole thing together. Anyway, let’s make this test pass:
class KataStringCalculator(formula: String) {
fun calc(): String {
return ""
}
}Ok, I think that would work. So when doing this, do I run the tests and then commit? No, I think the point is to be brazenly confident and just go. If it doesn’t work, you type it again. So:
$ git commit -am "empty formula gives empty result"
Running pre-commit hook
Running gradle tests...
BUILD SUCCESSFUL in 663ms
3 actionable tasks: 3 up-to-date
Pre-commit hook completed successfully
[main d987518] empty formula gives empty result
2 files changed, 14 insertions(+), 9 deletions(-)
create mode 100644 app/src/main/kotlin/KataStringCalculator.ktYay! It worked. Ok, any refactoring needed here? One thing I see is the test itself is kind of a long line, lets refactor it:
@Test
fun emptyStringShouldReturnEmptyString() {
KataStringCalculator("")
.calc()
.shouldBeEmpty()
}and we run the commit and get:
@Test
fun emptyStringShouldReturnEmptyString() {
KataStringCalculator("")
.calc()
.shouldBeEmpty()
}which succeeds again (would have surprised me otherwise):
$ git commit -am "refactored test for easier writing"
Running pre-commit hook
Running gradle tests...
BUILD SUCCESSFUL in 602ms
3 actionable tasks: 3 up-to-date
Pre-commit hook completed successfully
[main 758d91b] refactored test for easier writing
12 files changed, 114 insertions(+) so that is good. Now lets do a single number:
@Test
fun singleNumberShouldReturnNumber() {
KataStringCalculator("1")
.calc()
.shouldBe("1")
}which we can make pass with:
class KataStringCalculator(val formula: String) {
fun calc(): String {
return formula;
}
}and trying to commit:
vextorspace@vNitro:~/repos/tcr$ git commit -am "handle single integer"
Running pre-commit hook
Running gradle tests...
BUILD SUCCESSFUL in 2s
3 actionable tasks: 3 executed
Pre-commit hook completed successfully
[main fa63f6d] handle single integer
2 files changed, 10 insertions(+), 2 deletions(-)cool, on a roll!
So what happens if we have a non-number? The kata states that non-numbers are interpreted as 0. So we want something that is not a number in a test.
@Test
fun nonNumberReturnsZero() {
KataStringCalculator("a")
.calc()
.shouldBe("0")
}So this should be relatively easy, if it is empty, return empty. If it is not a number, return zero, else return the input:
class KataStringCalculator(val formula: String) {
fun calc(): String {
return when {
formula.isEmpty() -> ""
formula.toIntOrNull() != null -> formula
else -> "0"
}
}
}hmmm… I feel a little nervouse there because that is a lot of typing. Wait, why did I feel the need to put in the formula.isEmpty()? that is why I feel nervous. Must have just started typing off the cuff!
I think that might be part of what this can teach you. To truly only type the code necessary to make it pass!
Lets try again:
class KataStringCalculator(val formula: String) {
fun calc(): String {
if(formula.toIntOrNull() == null) {
return "0"
}
return formula
}
}running the commit:
$ git commit -am "non-numbers give 0"
Running pre-commit hook
Running gradle tests...
> Task :app:test FAILED
KataStringCalculatorTest > emptyStringShouldReturnEmptyString() FAILED
java.lang.AssertionError at KataStringCalculatorTest.kt:10
4 tests completed, 1 failed
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ':app:test'.
> There were failing tests. See the report at: file:///home/vextorspace/repos/tcr/app/build/reports/tests/test/index.html
* Try:
> Run with --scan to get full insights.
BUILD FAILED in 2s
3 actionable tasks: 2 executed, 1 up-to-date
Tests failed! Removing changes...
HEAD is now at 784f853 non-numbers give 0oh no! It deleted my changes!
It failed the emptyStringShouldReturnEmptyString - because now it is returning 0. I need a guard clause, so that ends up being 3 different cases. I should just use a when like I first thought:
class KataStringCalculator(val formula: String) {
fun calc(): String {
return when {
formula.isEmpty() -> ""
formula.toIntOrNull() != null -> formula
else -> "0"
}
}
}sure enough that works.
class KataStringCalculator(val formula: String) {
fun calc(): String {
return when {
formula.isEmpty() -> ""
formula.toIntOrNull() != null -> formula
else -> "0"
}
}
}But that looks a little messy, probably why I was feeling nervous about it in the first place. Lets refactor a little:
class KataStringCalculator(val formula: String) {
fun calc(): String {
return when {
formula.isEmpty() -> ""
checkForNumber(formula) -> formula
else -> "0"
}
}
private fun checkForNumber(expression: String): Boolean {
return expression.toIntOrNull() != null
}
}ok, that is a little easier to read the intent. I kind of wish Kotlin was like Rust here and you didn’t need the return statement but whatever. I am also not convinced this is better than a guard clause and a final thing, less indentation then:
class KataStringCalculator(val formula: String) {
fun calc(): String {
if(formula.isEmpty()) return ""
if(checkForNumber(formula)) return formula
return "0"
}
private fun checkForNumber(expression: String): Boolean {
return expression.toIntOrNull() != null
}yah, that looks cleaner to me. I might feel different if there were a lot of guard clauses or if they were complicated.
Ok, what is next? Let’s add an operation:
@Test
fun onePlusOneIsTwo() {
KataStringCalculator("1+1")
.calc()
.shouldBe("2")
}So here we need to identify that there is an operation, split the string around it, extract the numbers, do the operation, and spit the result back as a string. Woah, not a simple easy step, is it? This to me sounds like an overall test, not a unit test. Maybe it is time for a second of design (oh, I just did that — maybe write it down on a todo list and commit that). Because one line of code is not going to make this pass. Oh, what if I write the test:
@Test
fun onePlusOneIsNotZero() {
KataStringCalculator("1+1")
.calc()
.shouldNotBe("0")
}This is a much smaller step.
fun calc(): String {
if(formula.isEmpty()) return ""
if(checkForOperation(formula)) return "1"
if(checkForNumber(formula)) return formula
return "0"
}
private fun checkForOperation(formula: String): Boolean {
return formula.contains("+")
}That should do it, right? Right??? let’s see…
$ git commit -am "formula 1+1 does not give 0"
Running pre-commit hook
Running gradle tests...
BUILD SUCCESSFUL in 561ms
3 actionable tasks: 3 up-to-date
Pre-commit hook completed successfully
[main c4b1bdd] formula 1+1 does not give 0
Date: Wed Oct 30 16:43:45 2024 -0700
2 files changed, 13 insertions(+), 1 deletion(-)and there was much rejoicing!
Ok, the next part of that is we need to do some testing on a formula parsing routine, so let’s refactor to put the routine in:
class KataStringCalculator(val formula: String) {
fun calc(): String {
if(formula.isEmpty()) {
return ""
}
if(checkForOperation(formula)) {
return parseFormula("1")
}
if(checkForNumber(formula)) {
return formula
}
return "0"
}
private fun parseFormula(formula: String): String {
return "1"
}
private fun checkForOperation(formula: String): Boolean {
return formula.contains("+")
}
private fun checkForNumber(expression: String): Boolean {
return expression.toIntOrNull() != null
}
}
ok, so now if we make the parse formula object its own class, it becomes easier to test and read because there is a lot going on. But is that early? I think it might be, so lets try writing another test with one of the sub-operations we know we need to do:
class ExpressionTest {
@Test
fun noOperationMeansNoSplit() {
Expression("1")
.split("+")
.shouldContainExactly(Expression("1"))
}
}which we try to make pass with:
data class Expression(val formula: String) {
fun split(operation: String): List<Expression> {
return emptyList()
}
}and does it work?… shoot. Try again. Oh, I returned an empty list instead of the expression! whoops. So we retype the test which takes a second but actually looks nicer because of better formatting. And then we type the proper answer:
data class Expression(val formula: String) {
fun split(operation: String): List<Expression> {
return listOf(Expression(formula))
}
}That worked! So what happens if we do have a plus? Back to the old 1+2:
@Test
fun splitOnPlusWithPlus() {
Expression("1+2")
.split("+")
.shouldContainExactly(
Expression("1"),
Expression("2")
)
}so we need to write a small bit of non-trivial code:
data class Expression(val formula: String) {
fun split(operation: String): List<Expression> {
return formula
.split(operation)
.map { Expression(it) }
}
}so this felt ok, I wasn’t worried. I knew I might lose it but also knew it wouldn’t take long to write those few lines again. But it worked! So now we can go write our original test:
@Test
fun onePlusTwoIsThree() {
KataStringCalculator("1+2")
.calc()
.shouldBe("3")
} note I decided to do 1 + 2 because that is a non-symmetric case so a little better in my opinion. And my attempt to make it pass:
private fun parseFormula(formula: String): String {
return Expression(formula).split("+")
.map { it.formula.toInt() }
.sum()
.toString()
}ok, I’ve actually tried twice and failed. This tells me I am still biting off too much. The real problem is my expressions can’t easily be added together. So let’s first extract an operation from the “+” and then we will add an operate to it. That is the small micro design decision I am making—TDD does not eliminate the need to make design decisions, it spreads them out into small decisions made at the last minute. After all, delayed design is less expensive than everything at the start.
class SumTest {
@Test
fun `sum of 1 and 2 is 3`() {
Sum("1", "2")
.calc()
.shouldBe(Expression("3"))
}
}so we write the sum class:
class Sum(val operand1: String, val operand2: String) {
fun calc(): Expression {
return Expression((operand1.toInt() + operand2.toInt()).toString())
}
}ok, that worked but needs a bit of refactoring:
class Sum(val operand1: String, val operand2: String) {
fun calc(): Expression {
val o1 = operand1.toInt()
val o2 = operand2.toInt()
val sum = o1 + o2
return Expression(sum.toString())
}
}a little longer but easier to read for sure!
So before I tackle the stitching it together, I see a bit more refactoring. I’d like to include the “+” in the sum operation, and that requires the sum operation to already exist so that requires I move the operands to the calculate. Or I put the “+” in a companion object. Maybe I do that instead. Anyway, after that I make another attempt at the parse of non-trivial summation:
private fun parseFormula(formula: String): String {
val expressions = Expression(formula)
.split(Sum.SYMBOL)
Sum(expressions[0].formula, expressions[1].formula)
.calc()
.let { return it.formula }
} drum roll please… YAY! it worked. Of course there is a lot left to improve on this, but I think that is enough to demonstrate how it should work. What did we learn? Is this a method that I would use on a regular basis? Almost. I think it needs some refinement. For starters, I think I want a bookmark in there. I want to be able to have it only go back as far as the written test that is failing. So make a failing test. Write some code. If it passes, commit. If not, revert the code but not the test. So that would be reverting the main src and resources directories, but not the src/test or src/test/resources directories. Would that have a bad effect on the outcome? Good question. We could always throw out the test too manually if we needed to with a
git reset —hard HEADBut does it violate the purposes of the TCR process in the first place? Let’s take a break and watch the TCR videos now that we have experimented ourselves…
<time goes by watching TCR Videos by Kent Beck>
It kind of does break the flow for what the manual version is trying to do. Hence so many
def function(self):
passentries.
Still, it does serve a purpose for what I want to do secretly, which is get an AI agent to use TCR to repeatedly try to make a single test pass. So that will be the next thing I try!


