Make The Dead Fish Swim - Test Driven Golang

by Damien Sedgwick, on 29 May 2022

Today we are going to take a look at solving "Make The Dead Fish Swim" - A Codewars challenge ranked '6 kyu' and we are going to do it using Test Driven Development.

First of all, let us look at the challenge we are going to tackle:

Write a simple parser that will parse and run Deadfish.

Deadfish has 4 commands, each 1 character long:

    i increments the value (initially 0)
    d decrements the value
    s squares the value
    o outputs the value into the return array

Invalid characters should be ignored.

Parse("iiisdoso") == []int{8, 64} // example output

A quick disclaimer, there is a good chance that there is a very concise and clever solution to this challenge, and I will share it at the end of this post. However my goal here is to practice taking a problem, and breaking it down into small testable pieces of functionality that could be used elsewhere (if we were building something in real life).

We are going to be working in two different files, main.go and main_test.go

I will also be using a package that I like to use when writing tests and that package is https://github.com/stretchr/testify

For me, a good starting point is to write out some test functions for the bits of the challenge that are repeated throughout the program, increment one, decrement one, square and outputs to the array. Although for now, I will start with the first three.

func TestIncrementOne(t *testing.T) {
	assert.Equal(t, 1, IncrementOne(0))
	assert.Equal(t, 5, IncrementOne(4))
	assert.Equal(t, 100, IncrementOne(99))
}

func TestDecrementOne(t *testing.T) {
	assert.Equal(t, 0, DecrementOne(1))
	assert.Equal(t, 4, DecrementOne(5))
	assert.Equal(t, 99, DecrementOne(100))
}

func TestSquare(t *testing.T) {
	assert.Equal(t, 1, Square(1))
	assert.Equal(t, 25, Square(5))
	assert.Equal(t, 64, Square(8))
}

Each of these functions will provide us with a specific piece of functionality that we will later need when parsing the input for the challenge. Let us run the tests.

// go test

./main_test.go:23:21: undefined: IncrementOne
./main_test.go:27:21: undefined DecrementOne
./main_test.go:31:21: undefined: Square
FAIL github.com/damiensedgwick/codewars/makeTheDeadFishSwim [build failed]

Pretty simple, they all fail because none of the functions actually exist yet, so let us create them.

func IncrementOne(n int) int {
	return n + 1
}

func DecrementOne(n int) int {
	return n - 1
}

func Square(n int) int {
	return n * n
}

These functions are very simple and may seem in fact trivial however they are going to give us the foundations we need to solve the challenge.

Let us run the tests again.

// go test

PASS
ok github.com/damiensedgwick/codewars/makeTheDeadFishSwim 0.195s

Much better, so what is next.

I think a good step would be to take the input string we are going to receive, and turn it in to an array of strings so that we can loop through it and decide what we would like to happen for each element of that array.

The test

func TestSplitString(t *testing.T) {
	assert.Equal(t, []string{
		"i",
		"i",
		"i",
		"s",
		"d",
		"o",
		"s",
		"o",
	}, SplitString("iiisdoso"))
}

If we run go test it will fail because this function does not yet exist, so let us create it.

func SplitString(s string) []string {
	return strings.Split(s, "")
}

Now our tests are happy again, we can proceed to the next step which could in fact be parsing the input. Let us write a test and see how we get on.

func TestParse(t *testing.T) {
	assert.Equal(t, []int{8, 64}, Parse("iiisdoso"))
}

The challenge is kind enough to give us a test case so we have used that to write the test and now we can try to create the function and make the test pass.

func Parse(input string) []int {
	var output = []int{}

	count := 0

	s := SplitString(input)
	for _, v := range s {
		switch v {
		case "i":
			count = IncrementOne(count)
		case "d":
			count = DecrementOne(count)
		case "s":
			count = Square(count)
		case "o":
			output = append(output, count)
		default:
			continue
		}
	}

	return output
}

So now we have our parse function, we can run the tests again and they should all be happy.

The tests are happy so I am going to take my code and paste it in to the codewars challenge, making sure to tweak variables names I used for the ones they specified and importing any packages that I used.

Let us take a look at the results

Test Results:
Random tests
random test #1 (`ossrshsbzotsdkpvuisekvcci`)
random test #2 (`odzosiqdshbiikiisiiyigdlzyd`)
random test #3 (`sokssndofoimuqbqipsedfodticuotosdso`)
random test #4 (`siiooisdisijoilusdshqddzosiotdoio`)
random test #5 (`oiiswwmaisdosisooitio`)
random test #6 (`sdhhidyoioimsoidioooodsiddoio`)
...
random test #94 (`dssdbiddoskuoondgirikdeuolspdjtsjkisido`)
random test #95 (`rsoboobssixzzoudfiinbddgm`)
random test #96 (`iooeioyoyviwsoqsisdhaixxl`)
random test #97 (`idsdodsfsdbxdismiskdsis`)
random test #98 (`icdzdsissuigoijvopdddpiddgsw`)
random test #99 (`diioidooooisssmdiiisiiosossiiohivvowud`)
random test #100 (`odooszoatitdwdibkdko`)

All test cases have passed and the challenge is complete! Now as mentioned, there is probably a very short and concise answer to this challenge which I will share below however I hope this post has given you an insight to how powerful test driven development can be.

package kata

func Parse(data string) []int{
  output := []int{}
  var val int
  for _, s := range data {
    switch string(s) {
      case "i":
        val++
      case "d":
        val--
      case "s":
        val = val * val
      case "o":
        output = append(output, val)
    }
  }
  
  return output
}

So in fact the solution is very similar to the one we implemented in this post. The only difference is we have the backing of our tests to give us absolute confidence in our programs functionality.