2

I'm attempting to implement testing with golden files, however, the JSON my function generates varies in order but maintains the same values. I've implemented the comparison method used here:

How to compare two JSON requests?

But it's order dependent. And as stated here by brad:

JSON objects are unordered, just like Go maps. If you're depending on the order that a specific implementation serializes your JSON objects in, you have a bug.

I've written some sample code that simulated my predicament:

package main

import (
    "bufio"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "math/rand"
    "os"
    "reflect"
    "time"
)

type example struct {
    Name     string
    Earnings float64
}

func main() {
    slice := GetSlice()
    gfile, err := ioutil.ReadFile("testdata/example.golden")
    if err != nil {
        fmt.Println(err)
        fmt.Println("Failed reading golden file")
    }

    testJSON, err := json.Marshal(slice)
    if err != nil {
        fmt.Println(err)
        fmt.Println("Error marshalling slice")
    }

    equal, err := JSONBytesEqual(gfile, testJSON)
    if err != nil {
        fmt.Println(err)
        fmt.Println("Error comparing JSON")
    }

    if !equal {
        fmt.Println("Restults don't match JSON")
    } else {
        fmt.Println("Success!")
    }
}

func GetSlice() []example {
    t := []example{
        example{"Penny", 50.0},
        example{"Sheldon", 70.0},
        example{"Raj", 20.0},
        example{"Bernadette", 200.0},
        example{"Amy", 250.0},
        example{"Howard", 1.0}}
    rand.Seed(time.Now().UnixNano())
    rand.Shuffle(len(t), func(i, j int) { t[i], t[j] = t[j], t[i] })
    return t
}

func JSONBytesEqual(a, b []byte) (bool, error) {
    var j, j2 interface{}
    if err := json.Unmarshal(a, &j); err != nil {
        return false, err
    }
    if err := json.Unmarshal(b, &j2); err != nil {
        return false, err
    }
    return reflect.DeepEqual(j2, j), nil
}

func WriteTestSliceToFile(arr []example, filename string) {
    file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

    if err != nil {
        fmt.Println("failed creating file: %s", err)
    }

    datawriter := bufio.NewWriter(file)
    marshalledStruct, err := json.Marshal(arr)
    if err != nil {
        fmt.Println("Error marshalling json")
        fmt.Println(err)
    }
    _, err = datawriter.Write(marshalledStruct)
    if err != nil {
        fmt.Println("Error writing to file")
        fmt.Println(err)
    }

    datawriter.Flush()
    file.Close()
}
Devyzr
  • 299
  • 5
  • 13
  • 5
    Are you experiencing an actual problem, or pre-emotively addressing this? Modern Go versions output deterministic JSON to allow such comparisons to be done more easily. Having said that, my normal approach is to unmarshal both structs (golden and actual) to a `map[string]interface{}`, then compare with [go-cmp](https://godoc.org/github.com/google/go-cmp/cmp) or similar. – Jonathan Hall Nov 21 '19 at 21:38
  • 1
    Comparing raw JSON is nearly always the wrong way to go about this - it's better to compare the structures that are semantically relevant to your application. – Luke Joshua Park Nov 21 '19 at 21:44
  • @Flimzy, I'm having a problem. My code obtains the information I want to process from a DB and I want to make sure it's working properly with a test, but for some reason when I convert the slice of structs into a JSON it doesn't keep it's order, even though the data fetched from db is identical. I'll give your solution a try and see how it goes :) – Devyzr Nov 21 '19 at 22:13
  • It's MSSQL, so the order isn't guaranteed, but it's usually the same. I don't need the results to be in order and later on will be comparing some JSON that will not maintain order due to being stored in a map. I only need to make sure the values are the same. – Devyzr Nov 21 '19 at 22:39

2 Answers2

5

JSON arrays are ordered. The json.Marshal function preserves order when encoding a slice to a JSON array.

JSON objects are not ordered. The json.Marshal function writes object members in sorted key order as described in the documentation.

The bradfitz comment JSON object ordering is not relevant to this question:

  • The application in the question is working with a JSON array, not a JSON object.
  • The package was updated to write object fields in sorted key order a couple of years after Brad's comment.

To compare slices while ignoring order, sort the two slices before comparing. This can be done before encoding to JSON or after decoding from JSON.

sort.Slice(slice, func(i, j int) bool {
     if slice[i].Name != slice[j].Name {
        return slice[i].Name < slice[j].Name
     }
     return slice[i].Earnings < slice[j].Earnings
})
Community
  • 1
  • 1
  • Thanks for explaining so well! I just have a question: how would I go about changing my array to an object? Would it be more efficient this way? – Devyzr Nov 22 '19 at 19:17
  • If the `Name` field is unique, then you can transform the slice of `example` to `map[string]float64` where the key is the `Name` and the value is `Earnings`. The performance gain for the six or so records is negligible. If the data is naturally a slice, then keep the code as is and sort. –  Nov 22 '19 at 21:33
  • I just realized I've been understanding this wrong the whole time. By JSON object you mean the struct, and by JSON array you mean the array of structs. That makes more sense, I thought ```json.Marshal``` was returning a JSON object of some type. – Devyzr Nov 23 '19 at 00:08
  • 1
    I am referring to object and array as defined in the JSON RFC (see links in the answer). Marshal returns JSON text per the terminology of the RFC. –  Nov 26 '19 at 06:35
3

For unit testing, you could use assert.JSONEq from Testify. If you need to do it programatically, you could follow the code of the JSONEq function.

https://github.com/stretchr/testify/blob/master/assert/assertions.go#L1708

user828878
  • 398
  • 4
  • 8