In a post I mentioned that when learning a new programming language we should start out making projects and tests for our code.
One language I am interested in is Go. Here I will list the steps to make a Go project with some tests. I will also include the source code for each file. I know this is redundant since there is a tutorial for this on the Go language site, but I wanted to make one with multiple packages. [Note 1]
First cd to a directory where you will create your project.
go.dir$ mkdir spearhead
go.dir$ cd spearhead
go.dir/spearhead$ go mod init info/shelfunit/spearhead
go: creating new go.mod: module info/shelfunit/spearhead
go.dir/spearhead$ more go.mod
module info/shelfunit/spearhead
go 1.18
NOTE: You must use slashes, not dots. I put in dots the first time, and it did not work.
First I will make a few functions that do some simple things with numbers. I will make a couple that add two numbers, and then add one to the total. I will make another function that subtracts one from the total. I will do the same for multiplication.
mkdir numberstuff
emacs -nw numberstuff/addition_enhancements.go
Here is the contents of the file:
package numberstuff
func AddOneToSum( x, y int ) int {
return x + y + 1
}
func SubtractOneFromSum( x, y int ) int {
return x + y - 1
}
Now I will write a few tests for this. This will go in numberstuff/addition_enhancements_test.go. Since I am using Emacs, I will create the new buffer while in numberstuff/addition_enhancements.go. I will probably not have too many Emacs commands going forward. Also: I am not too familiar with the Go toolchain or the Emacs Go mode, so I will be running some tests and commands on the command line. When my Emacs-fu is more powerful, I will be able to do it all in Emacs.
package numberstuff
import (
"fmt"
"testing"
)
func Test_AddOneToSum( t *testing.T ) {
fmt.Println("Testing AddOneToSum")
result := AddOneToSum(3, 4)
if result != 8 {
t.Error("Incorrect result, expected 8, got ", result)
}
}
func Test_AddOneToSumCases(t *testing.T) {
fmt.Println("Testing AddOneToSumCases")
cases := []struct {
a, b, result int
}{
{3, 4, 8},
{4, 5, 10},
{5, 6, 12},
}
for _, c := range cases {
got := AddOneToSum(c.a, c.b)
if (got != c.result) {
t.Errorf( "incorrect result: AddOneToCases(%x, %x) gave %x, wanted %x", c.a, c.b, got, c.result )
}
}
}
func Test_SubtractOneFromSum( t *testing.T ) {
result := SubtractOneFromSum(3, 4)
if result != 6 {
t.Error("Incorrect result, expected 6, got ", result)
}
}
Now I will do the same for multiplication. Here is numberstuff/multiplication_enhancements.go:
package numberstuff
func AddOneToProduct( x, y int ) int {
return (x * y) + 1
}
func SubtractOneFromProduct( x, y int ) int {
return (x * y) - 1
}
Here is numberstuff/multiplication_enhancements_test.go:
package numberstuff
import (
"fmt"
"testing"
)
func Test_AddOneToProduct( t *testing.T ) {
fmt.Println("Testing AddOneToProduct")
result := AddOneToProduct(3, 4)
if result != 13 {
t.Error("Incorrect result, expected 13, got ", result)
}
}
func Test_SubtractOneFromProduct( t *testing.T ) {
result := SubtractOneFromProduct(3, 4)
if result != 11 {
t.Error("Incorrect result, expected 11, got ", result)
}
}
Next I will invoke these from my main file, spearhead.go:
package main
import (
"fmt"
"info/shelfunit/spearhead/numberstuff"
)
func main() {
fmt.Println("Hello, world.")
fmt.Println("about to call numberstuff.AddOneToSum( 5, 6): ", numberstuff.AddOneToSum( 5, 6 ) )
fmt.Println("about to call numberstuff.SubtractOneFromSum( 5, 6 ): ", numberstuff.SubtractOneFromSum( 5, 6 ) )
fmt.Println("about to call numberstuff.AddOneToProduct( 5, 6): ", numberstuff.AddOneToProduct( 5, 6 ) )
fmt.Println("about to call numberstuff.SubtractOneFromProduct( 5, 6 ): ", numberstuff.SubtractOneFromProduct( 5, 6 ) )
}
Now run this from the command line.
go build
This will create an executable called “spearhead” next to “spearhead.go”. There are two ways I can run this program:
go run spearhead.go
Or just
./spearhead
Either way gives the following:
Hello, world.
about to call numberstuff.AddOneToSum( 5, 6): 12
about to call numberstuff.SubtractOneFromSum( 5, 6 ): 10
about to call numberstuff.AddOneToProduct( 5, 6): 31
about to call numberstuff.SubtractOneFromProduct( 5, 6 ): 29
To run the test, I need to go into the numberstuff directory:
cd numbertest
go test
Here is the result:
Testing AddOneToSum
Testing AddOneToSumCases
Testing AddOneToProduct
PASS
ok info/shelfunit/spearhead/numberstuff 0.003s
I put in some calls to fmt to print out stuff to the command line so I know it is working. For that, we have “go test -v”:
go test -v
=== RUN Test_AddOneToSum
Testing AddOneToSum
--- PASS: Test_AddOneToSum (0.00s)
=== RUN Test_AddOneToSumCases
Testing AddOneToSumCases
--- PASS: Test_AddOneToSumCases (0.00s)
=== RUN Test_SubtractOneFromSum
--- PASS: Test_SubtractOneFromSum (0.00s)
=== RUN Test_AddOneToProduct
Testing AddOneToProduct
--- PASS: Test_AddOneToProduct (0.00s)
=== RUN Test_SubtractOneFromProduct
--- PASS: Test_SubtractOneFromProduct (0.00s)
PASS
ok info/shelfunit/spearhead/numberstuff 0.003s
If I want to run the test from the root of the project, run go test with the relative path to the project, with or without “-v” as desired:
go test ./numberstuff
ok info/shelfunit/spearhead/numberstuff 0.002s
Just to make sure I am not going crazy, let’s change one of the result assertions in numberstuff/addition_enhancements_test.go from 8 to 9:
func Test_AddOneToSum( t *testing.T ) {
fmt.Println("Testing AddOneToSum")
result := AddOneToSum(3, 4)
if result != 9 {
t.Error("Incorrect result, expected 9, got ", result)
}
}
That gives us this:
go test ./numberstuff
Testing AddOneToSum
--- FAIL: Test_AddOneToSum (0.00s)
addition_enhancements_test.go:12: Incorrect result, expected 9, got 8
Testing AddOneToSumCases
Testing AddOneToProduct
FAIL
FAIL info/shelfunit/spearhead/numberstuff 0.003s
FAIL
Change it back before moving forward.
To prove that I know what I am doing, I will add another package: morestrings. This is the same package that is used in How To Write Go, but I am going to make a different function.
I will make a file morestrings/duplicate_string.go:
package morestrings
func ReturnStringTwice(s string) string {
return s + s
}
Next I will make a test file morestrings/duplicate_string_test.go:
package morestrings
import (
"fmt"
"testing"
)
func TestDuplicateString(t *testing.T) {
fmt.Println( "Starting TestDuplicateString" )
cases := []struct {
in, want string
}{
{"Hello, world", "Hello, worldHello, world"},
{"eating nun arrays funny", "eating nun arrays funnyeating nun arrays funny"},
{"negative houses gauge freedom", "negative houses gauge freedomnegative houses gauge freedom"},
}
for _, c := range cases {
fmt.Println( "c.in: ", c.in, ", c.want: ", c.want )
got := ReturnStringTwice(c.in)
if got != c.want {
t.Errorf("ReturnStringTwice(%q) == %q, want %q", c.in, got, c.want)
}
}
}
Now we run the tests:
go test -v ./morestrings/
=== RUN TestDuplicateString
Starting TestDuplicateString
c.in: Hello, world , c.want: Hello, worldHello, world
c.in: eating nun arrays funny , c.want: eating nun arrays funnyeating nun arrays funny
c.in: negative houses gauge freedom , c.want: negative houses gauge freedomnegative houses gauge freedom
--- PASS: TestDuplicateString (0.00s)
PASS
ok info/shelfunit/spearhead/morestrings (cached)
Now we update spearhead.go:
package main
import (
"fmt"
"info/shelfunit/spearhead/morestrings"
"info/shelfunit/spearhead/numberstuff"
)
func main() {
fmt.Println("Hello, world.")
fmt.Println("about to call numberstuff.AddOneToSum( 5, 6): ", numberstuff.AddOneToSum( 5, 6 ) )
fmt.Println("about to call numberstuff.SubtractOneFromSum( 5, 6 ): ", numberstuff.SubtractOneFromSum( 5, 6 ) )
fmt.Println("about to call numberstuff.AddOneToProduct( 5, 6): ", numberstuff.AddOneToProduct( 5, 6 ) )
fmt.Println("about to call numberstuff.SubtractOneFromProduct( 5, 6 ): ", numberstuff.SubtractOneFromProduct( 5, 6 ) )
fmt.Println("about to call morestrings.ReturnStringTwice(\"twice\"): ", morestrings.ReturnStringTwice( "twice" ) )
}
We can run all the tests with this command:
go test -v ./...
That is a period, a slash, and three periods. Sometimes the generator and browser make the three periods into one character.
I know we should be open to new ideas and new ways of doing things, but I wish “go test” just ran all the tests in the project tree. Just like Maven and Gradle. And Leiningen. And Clojure CLI tools And Ruby. And Elixir. Before anyone tries to play the “because Ken Effing Thompson sez so” card, I already do that with the ternary operator: he is the guy who put it in C, and he kept it out of Go.
You’re welcome.
Note 1: I have also been trying to do this with Elixir. For some reason, I have been able to make a few different modules, but I cannot get the tests to run. I also do not get any errors for the tests. I will step through the relevant chapter in Dave Thomas’ book and post about Elixir in the near future.
Image from Évangéliaire de Saint-Mihiel, aka Irmengard Codex, a 10th century manuscript held at the Colleges of the Université Catholique de Lille; image from Wikimedia, assumed allowed under public domain. “Irmengard, it’s a cerdex.”