Skip to content

Instantly share code, notes, and snippets.

@StevenACoffman
Last active October 27, 2023 10:30
Show Gist options
  • Save StevenACoffman/f5eb82ab32aa567f5d16506c53324971 to your computer and use it in GitHub Desktop.
Save StevenACoffman/f5eb82ab32aa567f5d16506c53324971 to your computer and use it in GitHub Desktop.
HTTP testing in Go

Goal: Less Badness

  • Enable Quality
  • Enable Maintenance
  • Enable Validation

Testing Terms

  • Stub - an object that provides predefined answers to method calls.
  • Mock - an object on which you set expectations.
  • Fake - an object with limited capabilities (for the purposes of testing), e.g. a fake web service.

Test Double is the general term for stubs, mocks and fakes. A mock is single use, but a fake can be reused.

  • Test Fixture - a well known and fixed environment in which tests are run so that results are repeatable. Some people call this the test context.
Examples of fixtures:
  • Preparation of input data and set-up/creation of fake or mock objects
  • Loading a database with a specific, known set of data
  • Erasing a hard disk and installing a known clean operating system installation
  • Copying a specific known set of files

Test fixtures contribute to setting up the system for the testing process by providing it with all the necessary data for initialization. The setup using fixtures is done to satisfy any preconditions there may be for the code under test. Fixtures allow us to reliably and repeatably create the state our code relies on upon without worrying about the details.

HTTP client testing techniques:

  1. Using httptest.Server: httptest.Server allows us to create a local HTTP server and listen for any requests. When starting, the server chooses any available open port and uses that. So we need to get the URL of the test server and use it instead of the actual service URL.

  2. Accept a Doer as a parameter The Doer is a single-method interface, as is often the case in Go:

    type Doer interface {
        Do(*http.Request) (*http.Response, error)
    }
    
  3. By Replacing http.Transport: Transport specifies the mechanism by which individual HTTP requests are made. Instead of using the default http.Transport, we’ll replace it with our own implementation. To implement a transport, we’ll have to implement http.RoundTripper interface. From the documentation:

    func Test_Mine(t *testing.T) {
        ...
        client := httpClientWithRoundTripper(http.StatusOK, "OK")
        ...
    }
    
    type roundTripFunc func(req *http.Request) *http.Response
    
    func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
    	return f(req), nil
    }
    
    func httpClientWithRoundTripper(statusCode int, response string) *http.Client {
    	return &http.Client{
    		Transport: roundTripFunc(func(req *http.Request) *http.Response {
    			return &http.Response{
    				StatusCode: statusCode,
    				Body:       ioutil.NopCloser(bytes.NewBufferString(response)),
    			}
    		}),
    	}
    }
    

HTTP client recorder:

Stuff

Libraries

Golden files:

Golden Helpers:

Replay prod traffic in test env:

Other articles

Dependency Injection

Accepting interfaces, return structs may sound so foreign when you first hear it. However, it can be boiled down to 2 main concepts

  • Let the consumer define the interfaces it uses
  • Producers should return concrete types

From https://stackoverflow.com/questions/47134293/compare-structs-except-one-field-golang/47136278#47136278

1. With embedding

One option would be to group fields that should take part in the comparison into a different struct which you can embed in your original. When doing the comparison, just compare the embedded fields:

type Person struct {
	Name string
	Age  int
}

type Doc struct {
	Person
	Created time.Time
}

func main() {
	d1 := Doc{
		Person:  Person{"Bob", 21},
		Created: time.Now(),
	}
	time.Sleep(time.Millisecond)
	d2 := Doc{
		Person:  Person{"Bob", 21},
		Created: time.Now(),
	}

	fmt.Println(d1 == d2)               // false
	fmt.Println(d1.Person == d2.Person) // true
}

Try it on the Go Playground.

If the struct does not contain pointers, slices, maps etc., you can simply compare the embedded values using ==. Otherwise use reflect.DeepEqual() to compare them.

2. Temporarily modifying the excludable fields

You may also choose to temporarily modify the fields you don't want to compare: make them equal so the comparison result will only depend on the rest of the fields:

a := test{"testName", time.Now().Format(time.StampMilli)}
time.Sleep(time.Millisecond)
b := test{"testName", time.Now().Format(time.StampMilli)}

// Save and make excluded fields equal:
old := a.time
a.time = b.time

fmt.Println(a.name == b.name)        // true
fmt.Println(reflect.DeepEqual(a, b)) // true

// Restore:
a.time = old

Try it on the Go Playground.

Another variation is to make a copy of one of the struct values, and modify and compare that to the other, so no need to restore the original, also this is "more concurrent-friendly":

// Make copies and make excluded fields equal:
a2 := a
a2.time = b.time

fmt.Println(a2.name == b.name)        // true
fmt.Println(reflect.DeepEqual(a2, b)) // true

Try this on the Go Playground.

3. Implement your own, custom comparison

If you can't or don't want to go with the above solutions, you can always create your own:

func compare(a, b test) bool {
	return a.name == b.name
}


fmt.Println(a.name == b.name) // true
fmt.Println(compare(a, b))    // true

Try this on the Go Playground.

Notes:

This isn't "friendly" at first, as the custom compare() function requires you to check all involved fields, but its implementation may use the above methods, e.g. (try it on the Go Playground):

func compare(a, b test) bool {
	a.time = b.time // We're modifying a copy, so no need to make another copy
	return reflect.DeepEqual(a, b)
}

You could also pass pointers to compare() to avoid copying the original struct, e.g. (try it on the Go Playground):

fmt.Println(a.name == b.name) // true
fmt.Println(compare(&a, &b))  // true

func compare(a, b *test) bool {
	a2 := new(test)
	*a2 = *a
	a2.time = b.time
	return reflect.DeepEqual(a2, b)
}

4. If your fields are exported, you may use reflect to check them. E.g.:

package main

import (
	"fmt"
	"reflect"
)

type Foo struct {
	Name string
	Date int
}

func (f *Foo) EqualExcept(other *Foo, ExceptField string) bool {
	val := reflect.ValueOf(f).Elem()
	otherFields := reflect.Indirect(reflect.ValueOf(other))

	for i := 0; i < val.NumField(); i++ {
		typeField := val.Type().Field(i)
		if typeField.Name == ExceptField {
			continue
		}

		value := val.Field(i)
		otherValue := otherFields.FieldByName(typeField.Name)

		if value.Interface() != otherValue.Interface() {
			return false
		}
	}
	return true
}

func main() {
	f := &Foo{
		"Drew",
		30,
	}

	f2 := &Foo{
		"Drew",
		50,
	}

	fmt.Println(f.EqualExcept(f2, "Date"))
	fmt.Println(f.EqualExcept(f2, "Name"))

}

https://play.golang.org/p/DlhE8aZGwa

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment