Design - Software Engineering

Strict mode

Failing early is good for many reasons and strict mode design makes this very helpful during testing. An http client is a good place for this design.

Client

HTTP clients issue requests to some service, mainly by the Do method. Define the Strict interface to match that of the familiar testing T.Fatal.

type Client struct {
	Strict
}

type Strict interface {
	Fatal(args ...interface{})
}

type StrictFunc func(...interface{})

func (s StrictFunc) Fatal(args ...interface{}) {
	s.Fatal(args...)
}

Once the client has the strict ability it can be used in it's methods. Default the client to a lax mode where the Fatal method does nothing.

func NewClient() *Client {
	return &Client{
		Strict: lax,
	}
}

var lax = StrictFunc(func(...interface{}) {})

Let's assume your service only accepts json and expects each request to set the correct header. A simple wrapper around http.DefaultClient could look like this.

Use the strict wrapper in public methods.
Private funcs just return errors as usual.
func (c *Client) Do(r *http.Request) (*http.Response, error) {
	if err := c.checkContentType(r); err != nil {
		c.Fatal(err)
		return nil, err
	}
	resp, err := http.DefaultClient.Do(r)
	c.Fatal(err)
	return resp, err
}

func (c *Client) checkContentType(r *http.Request) error {
	got := r.Header.Get("Content-Type")
	exp := "application/json"
	if got != exp {
		return fmt.Errorf("checkContentType: %q must be %s", got, exp)
	}
	return nil
}

Any error from the sending of the request will be checked by the strict interface. This adds no real benefit to the client itself but it makes a difference when testing.

func TestClient(t *testing.T) {
	c := NewClient()
	c.Strict = t
	r, _ := http.NewRequest("GET", "nothing", nil)
	c.Do(r)
}
$ go test
--- FAIL: TestClient (0.00s)
    strictClient.go:32: checkContentType: "" must be application/json

Descriptive error messages make tests short and concise. Use check prefix to distinguish from asserts.