Go Unit Tests with JSON Compare

Go Unit Tests with JSON Compare

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.

Simple Calculation Tests

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:

go
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:

go
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:

go
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.

Tests for Functions with Complex Output

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:

go
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:

go
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:

Go Unit Test Optimization

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:

go
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:

go
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 Compare Tests for Functions with Complex Output

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:

  • FullMatch – means items are identical.
  • SupersetMatch – means first item is a superset of a second item.
  • NoMatch – means objects are different.

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:

json
{"a": 1, "b": 2, "c": 3}

Is a superset of (or second item is a subset of a first one):

json
{"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:

go
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:

Go Unit Test Optimization with JSON

If we are satisfied with the output, we copy and paste it into the expected JSON output string for the test:

go
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:

go
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:

JSON Go Unit Testing Optimization

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:

go
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):

go
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:

go
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)
}

Conclusions

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.