Golang continues to increase in popularity over time because it is both fairly easy to use, and it comes packed with features. This “everything but the kitchen sink” approach Go offers is one of the facets that make programming in the language so convenient.
This is also true of writing unit tests - in general, Go simplifies their creation. Testing individual features, however, can be rather complicated, and sometimes, it may take more time to create unit tests than to write the code itself. Once the unit tests are implemented as well, they need to be maintained and updated for the entire life of a project. Testing in perpetuity like this can result in significant maintenance efforts, so it is best practice to develop techniques to simplify and speed up that process.
As a software development company, we optimize our test development in Go with a helpful function called JSON Compare, which we have provided for you below. We’ll walk through why this function is useful and how it can streamline the building of unit tests and save you time.
It is relatively straightforward to create unit tests for functions that have primitive data type results. For example, we have the following function which rounds up a number by a given percentage to the nearest whole number:
package simple
import (
"math"
)
// TakePart - gives the nearest whole greater number from total by the percentage.
func TakePart(percentage float64, total int64) (part int64) {
if percentage < 0 { percentage = 0 } if percentage > 100 {
percentage = 100
}
return int64(math.Ceil(percentage * float64(total) / 100))
}
The unit tests will look like this:
package simple
import "testing"
func TestTakePart(t *testing.T) {
tests := []struct {
percentage float64
total int64
expectedPart int64
}{
{100, 100, 100},
{95.5, 100, 96},
{-10.4, 100, 0},
{150, 100, 100},
{0.5, 100, 1},
{30, 5, 2},
}
for _, test := range tests {
if result := TakePart(test.percentage, test.total); result != test.expectedPart {
t.Errorf("expected %d but have %d", test.expectedPart, result)
}
}
}
If we change the function later to provide a different behavior (rounding down, for example), then it will be quite easy to modify the failing test:
package simple
import "testing"
func TestTakePart(t *testing.T) {
tests := []struct {
percentage float64
total int64
expectedPart int64
}{
{100, 100, 100},
{95.5, 100, 95},
{-10.4, 100, 0},
{150, 100, 100},
{0.5, 100, 0},
{30, 5, 1},
}
for _, test := range tests {
if result := TakePart(test.percentage, test.total); result != test.expectedPart {
t.Errorf("expected %d but have %d", test.expectedPart, result)
}
}
}
We simply change the expected int64 number to the new one required by the updated contract for the function.
Now let’s consider a more complicated case with a slice of structures as the output. This function will make a copy of the input structure and sort that structure by two fields:
package simple
import "sort"
type Figure struct {
Type string
Color string
}
// Sort - makes a copy of a slice of figures and sorts them.
func Sort(in []Figure) (out []Figure) {
if in == nil {
return
}
out = make([]Figure, len(in))
copy(out, in)
sort.Slice(out, func(i, j int) bool {
if out[i].Type == out[j].Type {
return out[i].Color < out[j].Color
}
return out[i].Type < out[j].Type
})
return
}
To test the contract for this function, we define the input array of structures, the desired output array of structures, and compare the two:
package simple
import (
"reflect"
"testing"
)
func TestSort(t *testing.T) {
in := []Figure{
{"circle", "white"},
{"square", "black"},
{"circle", "black"},
{"square", "white"},
{"square", "red"},
}
expected := []Figure{
{"circle", "black"},
{"circle", "white"},
{"square", "black"},
{"square", "red"},
{"square", "white"},
}
out := Sort(in)
if !reflect.DeepEqual(expected, out) {
t.Fatalf("the expected result %v is not equal to what we have %v", expected, out)
}
}
Say we have something wrong with the expected or real output, so the arrays are different. In this case, the test will not display the exact differences between the arrays:
Therefore, we will still have to compare the output arrays manually or with some external tool, update the incorrect array, and run the test again.
Now, what happens if we add a new field, “Dimension”, to the “Figure” structure? We also want to modify the sorting function to have the “Dimension” field sorted first:
package simple
import "sort"
type Figure struct {
Dimension int
Type string
Color string
}
// Sort - makes a copy of a slice of figures and sorts them.
func Sort(in []Figure) (out []Figure) {
if in == nil {
return
}
out = make([]Figure, len(in))
copy(out, in)
sort.Slice(out, func(i, j int) bool {
if out[i].Dimension == out[j].Dimension {
if out[i].Type == out[j].Type {
return out[i].Color < out[j].Color
}
return out[i].Type < out[j].Type
}
return out[i].Dimension < out[j].Dimension
})
return
}
We need to update the expected output manually and retest. The process usually succeeds in a few iterations, but this can be time-consuming and error-prone. If the test results are complex data structures then this can be a very painful process to get right:
package simple
import (
"reflect"
"testing"
)
func TestSort(t *testing.T) {
in := []Figure{
{2, "circle", "white"},
{2, "square", "black"},
{3, "cone", "black"},
{2, "circle", "black"},
{2, "square", "white"},
{3, "cone", "white"},
{2, "square", "red"},
{3, "cube", "black"},
}
expected := []Figure{
{2, "circle", "black"},
{2, "circle", "white"},
{2, "square", "black"},
{2, "square", "red"},
{2, "square", "white"},
{3, "cone", "black"},
{3, "cone", "white"},
{3, "cube", "black"},
}
out := Sort(in)
if !reflect.DeepEqual(expected, out) {
t.Fatalf("the expected result %v is not equal to what we have %v", expected, out)
}
}
JSON can be used to radically simplify the process of outputting complex data. The basic idea is to serialize the output structures to JSON strings and then compare the JSON strings using a comparison tool. We found a very convenient library for that:
The main purpose of the library is integration into tests which use json and providing human-readable output of test results.
The lib can compare two json items and return a detailed report of the comparison.
At the moment it can detect a couple of types of differences:
Being a superset means that every object and array which don’t match completely in a second item must be a subset of a first item. For example:
{"a": 1, "b": 2, "c": 3}
Is a superset of (or second item is a subset of a first one):
{"a": 1, "c": 3}
Library API documentation can be found on godoc.org.
You can try LIVE version here (thanks to gopherjs):
https://nosmileface.dev/jsondiff
The library is inspired by http://tlrobinson.net/projects/javascript-fun/jsondiff/
Now, the unit tests can be written in two stages. Instead of defining the expected output, we simply print the output of the sorting function for the given inputs:
package simple
import (
"encoding/json"
"fmt"
"testing"
"github.com/nsf/jsondiff"
)
func TestSortJsonCompareStage1(t *testing.T) {
in := []Figure{
{"circle", "white"},
{"square", "black"},
{"circle", "black"},
{"square", "white"},
{"square", "red"},
}
expectedJsonStr := "[]"
out := Sort(in)
outJsonStr, err := json.MarshalIndent(out, "", " ")
if err != nil {
t.Fatal("error marshaling package", err)
}
fmt.Println(string(outJsonStr))
diffOpts := jsondiff.DefaultConsoleOptions()
res, diff := jsondiff.Compare([]byte(expectedJsonStr), []byte(outJsonStr), &diffOpts;)
if res != jsondiff.FullMatch {
t.Errorf("the expected result is not equal to what we have: %s", diff)
}
}
Then we run the test to get the output in formatted JSON directly in the console:
If we are satisfied with the output, we copy and paste it into the expected JSON output string for the test:
package simple
import (
"encoding/json"
"testing"
"github.com/nsf/jsondiff"
)
func TestSortJsonCompareStage2(t *testing.T) {
in := []Figure{
{"circle", "white"},
{"square", "black"},
{"circle", "black"},
{"square", "white"},
{"square", "red"},
}
expectedJsonStr := `
[
{
"Type": "circle",
"Color": "black"
},
{
"Type": "circle",
"Color": "white"
},
{
"Type": "square",
"Color": "black"
},
{
"Type": "square",
"Color": "red"
},
{
"Type": "square",
"Color": "white"
}
]
`
out := Sort(in)
outJsonStr, err := json.MarshalIndent(out, "", " ")
if err != nil {
t.Fatal("error marshaling package", err)
}
//fmt.Println(string(outJsonStr))
diffOpts := jsondiff.DefaultConsoleOptions()
res, diff := jsondiff.Compare([]byte(expectedJsonStr), []byte(outJsonStr), &diffOpts;)
if res != jsondiff.FullMatch {
t.Errorf("the expected result is not equal to what we have: %s", diff)
}
}
That is all! We did not even have to write the output array of the structures; it was printed by the test for us.
What happens if we want to modify the function and add primary sorting by a new “Dimension” field?
We follow the same process by modifying the input data and printing the output:
package simple
import (
"encoding/json"
"fmt"
"testing"
"github.com/nsf/jsondiff"
)
func TestSortJsonCompareStage1(t *testing.T) {
in := []Figure{
{2, "circle", "white"},
{2, "square", "black"},
{3, "cone", "black"},
{2, "circle", "black"},
{2, "square", "white"},
{3, "cone", "white"},
{2, "square", "red"},
{3, "cube", "black"},
}
expectedJsonStr := `
[
{
"Type": "circle",
"Color": "black"
},
{
"Type": "circle",
"Color": "white"
},
{
"Type": "square",
"Color": "black"
},
{
"Type": "square",
"Color": "red"
},
{
"Type": "square",
"Color": "white"
}
]
`
out := Sort(in)
outJsonStr, err := json.MarshalIndent(out, "", " ")
if err != nil {
t.Fatal("error marshaling package", err)
}
fmt.Println(string(outJsonStr))
diffOpts := jsondiff.DefaultConsoleOptions()
res, diff := jsondiff.Compare([]byte(expectedJsonStr), []byte(outJsonStr), &diffOpts;)
if res != jsondiff.FullMatch {
t.Errorf("the expected result is not equal to what we have: %s", diff)
}
}
Now we have the sorted structure as JSON in the console, and we also immediately see the exact difference between the expected and real results:
This gives us much more information than we had when we used reflect.DeepEqual in the first test example. Now we can see the exact difference any time the test fails, which is very convenient for diagnostics.
We simply copy the correct result and paste it back into the test as the expected value:
package simple
import (
"encoding/json"
"testing"
"github.com/nsf/jsondiff"
)
func TestSortJsonCompareStage2(t *testing.T) {
in := []Figure{
{2, "circle", "white"},
{2, "square", "black"},
{3, "cone", "black"},
{2, "circle", "black"},
{2, "square", "white"},
{3, "cone", "white"},
{2, "square", "red"},
{3, "cube", "black"},
}
expectedJsonStr := `
[
{
"Dimension": 2,
"Type": "circle",
"Color": "black"
},
{
"Dimension": 2,
"Type": "circle",
"Color": "white"
},
{
"Dimension": 2,
"Type": "square",
"Color": "black"
},
{
"Dimension": 2,
"Type": "square",
"Color": "red"
},
{
"Dimension": 2,
"Type": "square",
"Color": "white"
},
{
"Dimension": 3,
"Type": "cone",
"Color": "black"
},
{
"Dimension": 3,
"Type": "cone",
"Color": "white"
},
{
"Dimension": 3,
"Type": "cube",
"Color": "black"
}
]
`
out := Sort(in)
outJsonStr, err := json.MarshalIndent(out, "", " ")
if err != nil {
t.Fatal("error marshaling package", err)
}
// fmt.Println(string(outJsonStr))
diffOpts := jsondiff.DefaultConsoleOptions()
res, diff := jsondiff.Compare([]byte(expectedJsonStr), []byte(outJsonStr), &diffOpts;)
if res != jsondiff.FullMatch {
t.Errorf("the expected result is not equal to what we have: %s", diff)
}
}
We can even simplify the test further by extracting the code for comparing the JSON into a separate function (which we mentioned at the outset - feel free to use in your own tests):
package gojsonut
import (
"encoding/json"
"fmt"
"testing"
"github.com/nsf/jsondiff"
)
func JsonCompare(t *testing.T, result interface{}, expectedJsonStr string) {
outJsonStr, err := json.MarshalIndent(result, "", " ")
if err != nil {
t.Fatal("error marshaling the result: ", err)
}
diffOpts := jsondiff.DefaultConsoleOptions()
res, diff := jsondiff.Compare([]byte(expectedJsonStr), []byte(outJsonStr), &diffOpts;)
if res != jsondiff.FullMatch {
fmt.Println("The real output with ident --->")
fmt.Println(string(outJsonStr))
t.Errorf("The expected result is not equal to what we have: \n %s", diff)
}
}
Now our test has become very minimalistic and easy to maintain:
package simple
import (
"testing"
"github.com/guntenbein/gojsonut"
)
func TestSortJsonCompareStage3(t *testing.T) {
in := []Figure{
{2, "circle", "white"},
{2, "square", "black"},
{3, "cone", "black"},
{2, "circle", "black"},
{2, "square", "white"},
{3, "cone", "white"},
{2, "square", "red"},
{3, "cube", "black"},
}
expectedJsonStr := `
[
{
"Dimension": 2,
"Type": "circle",
"Color": "black"
},
{
"Dimension": 2,
"Type": "circle",
"Color": "white"
},
{
"Dimension": 2,
"Type": "square",
"Color": "black"
},
{
"Dimension": 2,
"Type": "square",
"Color": "red"
},
{
"Dimension": 2,
"Type": "square",
"Color": "white"
},
{
"Dimension": 3,
"Type": "cone",
"Color": "black"
},
{
"Dimension": 3,
"Type": "cone",
"Color": "white"
},
{
"Dimension": 3,
"Type": "cube",
"Color": "black"
}
]
`
out := Sort(in)
gojsonut.JsonCompare(t, out, expectedJsonStr)
}
The JSON Compare approach saves a great deal of time for our team when writing any tests for functions with complex output (big structures, slices of structures, maps with key/value pairs, etc.) Tests become easier to write, analyze, and maintain. This saves development time, which in turn allows us to write and maintain more tests, and increases the overall test coverage of an application.
All the examples from this post are taken from the repository, made especially for this article. Please feel free to incorporate the examples into your own code so you too can experience the simplicity and convenience of using JSON Compare in your tests.