GO Lang testing HTTP client and server
Huge thanks to Boldly Go for making quite short and exactly wanted videos
Testing HTTP client
Here is sample http client
package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
r := NewRepository("https://catfact.ninja/fact")
fact, err := r.GetRandomFactAboutCats()
if err != nil {
panic(err)
}
fmt.Println(fact)
}
type repository struct {
address string
client *http.Client
}
func NewRepository(address string) *repository {
return &repository{
address: address,
client: &http.Client{},
}
}
func (r *repository) GetRandomFactAboutCats() (string, error) {
resp, err := r.client.Get(r.address)
if err != nil {
return "", err
}
defer resp.Body.Close()
var data struct {
Fact string `json:"fact"`
}
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return "", err
}
return data.Fact, nil
}
notes:
- does not matter if we are passing
adress
or not, key point here is to be able to passhttp.Client
it self
And here are examples of two approaches
package main
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestWithServer(t *testing.T) {
want := "cats have nine lives"
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// here we may write as much conditions as we want
// e.g.:
// if r.Method != http.MethodGet { ... }
// if r.URL.Path != "/fact" { ... }
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]string{
"fact": want,
})
}))
defer s.Close()
// `s.URL` is the URL of the test server
// if, we would pass `http.Client` as a parameter to the repository
// we would pass `s.Client()` instead of `http.Client`
r := NewRepository(s.URL)
got, err := r.GetRandomFactAboutCats()
if err != nil {
t.Fatal(err)
}
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestWithFake(t *testing.T) {
client := &http.Client{
Transport: FakeService(func(req *http.Request) (*http.Response, error) {
// the same way, here we may introduce as much conditions as we want
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{"application/json"},
},
Body: io.NopCloser(strings.NewReader(`{"fact":"cats have nine lives"}`)),
}, nil
}),
}
// once again, at the very end the goal is to pass `http.Client`
r := &repository{
client: client,
}
got, err := r.GetRandomFactAboutCats()
if err != nil {
t.Fatal(err)
}
want := "cats have nine lives"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
// our fake service definition
type FakeService func(*http.Request) (*http.Response, error)
// with single method, that will be called instead of http.Client
func (fake FakeService) RoundTrip(req *http.Request) (*http.Response, error) {
return fake(req)
}
notes:
- in both cases, at the very end, we are passing
http.Client
so make sure it is "injectable"
Testing HTTP server
Second one is about HTTP servers, so here is our server example:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", hello)
log.Fatal(http.ListenAndServe(":8080", nil))
}
func hello(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintln(w, "Hello World")
}
and corresponding test
package main
import (
"io"
"net/http"
"net/http/httptest"
"testing"
)
func TestWithServer(t *testing.T) {
// note: we are bootstraping whole server here
s := httptest.NewServer(http.HandlerFunc(hello))
defer s.Close()
req, err := http.NewRequest(http.MethodGet, s.URL, nil)
if err != nil {
t.Fatal(err)
}
// note - we are using `http.DefaultClient` here, so actual requests will be sent
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
got := string(body)
want := "Hello World\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func TestWithRecorder(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", nil)
recorder := httptest.NewRecorder()
// instead of real request, we are calling our handler directly
// passing `recorder` as a response writer
hello(recorder, req)
// after handler is called we can extract response from recorder
res := recorder.Result()
// rest is the same as in the previous test
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
t.Fatal(err)
}
got := string(body)
want := "Hello World\n"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}