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
adressor not, key point here is to be able to passhttp.Clientit 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.Clientso 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)
}
}