March 21, 2019

My simplified development workflow in Go

Using tests during development to keep you code tidy

As I was working on a project, while developing a new function, I noticed that I was modifying some existing code to be able to test it. Doing it this way seemed like a very bad idea, so I looked for a better way to do it. But let’s start an insanely simple example, to clarify what I’m saying. We start with a basic program, an extraString function and a main.

// main.go
package main

import "fmt"

func extraString(s string) string {
	return fmt.Sprintf("Given: %s", s)
}

func main() {
	s := extraString("test")
	fmt.Println(s)
}

Now, I want to create a new function that returns a string (for the example, a hard coded one) and pass it to extraString. To make sure that it works, I have to get this new function hardString to run, and see that it actually returns what I want, in this case, the string Haaaard (as in hard coded string). So the first way I did this was to comment what was already in the main function, and just print out the return value

// main.go
package main

import "fmt"

func extraString(s string) string {
	return fmt.Sprintf("Given: %s", s)
}

func hardString() {
	return "Haaaard"
}

func main() {
	//s := extraString("test")
	//fmt.Println(s)
	fmt.Println(hardString())

}

Ugly, isn’t it ?

By modifying what was in main, I took the risks of:

  • possibly breaking what existed before
  • forgetting to uncomment and commiting to version control
  • delete this commented code thinking that it is just garbage code

I guess we could find a lot of other reasons.

Improving the process

But how could I try out this new piece of code, without touching my main, and keep incrementally improving on it ?

The first thought that popped up was to put it in a completely separate file, with only the piece of code that I am working on. I could then recycle this file and develop an other function in it. The problem with that, is that it is not always as easy as just copying the function you’re working on, since a program is made of so many different moving parts.

But then, I remembered that when you create tests in go, eqch test functions is called individually, so I can just call my new function from one of those tests, and print the result out.

//main.go
package main

import "fmt"

func extraString(s string) string {
	return fmt.Sprintf("Given: %s", s)
}

func hardString() string {
	return "Haaaard"
}

func main() {
	s := extraString("test")
	fmt.Println(s)
}
//main_test.go
package main

import (
	"fmt"
	"testing"
)

func TestHardString(t *testing.T) {
	fmt.Println(hardString())
}

When I then run the tests from the CLI, TestHardString is run, and I get what I expected

$ go test
Haaaard
PASS
ok      gitlab.com/mrngilles/scratchboard       0.002s

Freeing my mind of breaking the function

Now that I know that I can run my function by itself, and which output I want, I can just test automatically if what is returned by the function is actually what I expected.

//main_test.go
package main

import (
	"testing"
)

func TestHardString(t *testing.T) {
	if hardString() != "Haaaard" {
		t.Error("hardString should have been\"Haaaard\"")
	}
}

And just like that, I’ve got an automated test. Now, just write what you want the function to do in the test first, and you’re going straight down into TDD. While I am not very fan of TDD, it can be very interesting to try it out, especially as you are starting out in software development, being a good framework to make you more confident about your code. TDD is, while quite useful, not the silver bullet to all issues you will encounter while programming. It is, after all, an other part of the software.

This is probably not a perfect method, but I quite like it. And since I am going to write the call to a function to see what it gives me anyway, I can just make it “pretty”, give it what I expect to give me back, and be confident enough that my function does the main task it was crafted to do.

Developping this way, I can code more cleanly, without having to copy paste parts of the code just to try them out, I can keep what I used to try out the function, and I gain more time by not having to rewrite, or find again the scratch code that I wrote to get the function working, and by automating all the chain of testing.

These conclusions may seem obvious, but having the possibility to explore the output of my function without even having finished it, calling it and printing some value from inside it while I’m coding is very interesting to me. You may already be doing something like this, in this case you probably did not learn anything, and maybe you know of even better ways to do this. If you learned something, please let me know !

Copyright Marin Gilles 2019-2022