Skip to content

Commit 1229cc6

Browse files
authored
Improve tests for streaming response endpoints (#105)
A few small improvements to the test suite: - Ensure `/stream` and `/stream-bytes` endpoints actually use `Transfer-Encoding: chunked`, instead of inferring/hoping based on Content-Length header - Explicitly test that `/drip` actually does sleep between writing bytes, rather than inferring that it does based on entire response duration
1 parent 5fffb29 commit 1229cc6

File tree

1 file changed

+94
-34
lines changed

1 file changed

+94
-34
lines changed

httpbin/handlers_test.go

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,22 @@ func assertStatusCode(t *testing.T, w *httptest.ResponseRecorder, code int) {
5151
}
5252
}
5353

54-
func assertHeader(t *testing.T, w *httptest.ResponseRecorder, key, val string) {
54+
// assertHeader asserts that a header key has a specific value in a
55+
// response-like object. x must be *httptest.ResponseRecorder or *http.Response
56+
func assertHeader(t *testing.T, x interface{}, key, want string) {
5557
t.Helper()
56-
if w.Header().Get(key) != val {
57-
t.Fatalf("expected header %s=%#v, got %#v", key, val, w.Header().Get(key))
58+
59+
var got string
60+
switch r := x.(type) {
61+
case *httptest.ResponseRecorder:
62+
got = r.Header().Get(key)
63+
case *http.Response:
64+
got = r.Header.Get(key)
65+
default:
66+
t.Fatalf("expected *httptest.ResponseRecorder or *http.Response, got %t", x)
67+
}
68+
if want != got {
69+
t.Fatalf("expected header %s=%#v, got %#v", key, want, got)
5870
}
5971
}
6072

@@ -1705,31 +1717,30 @@ func TestStream(t *testing.T) {
17051717
test := test
17061718
t.Run("ok"+test.url, func(t *testing.T) {
17071719
t.Parallel()
1708-
r, _ := http.NewRequest("GET", test.url, nil)
1709-
w := httptest.NewRecorder()
1710-
app.ServeHTTP(w, r)
1720+
srv := httptest.NewServer(app)
1721+
defer srv.Close()
17111722

1712-
// TODO: The stdlib seems to automagically unchunk these responses
1713-
// and I'm not quite sure how to test this:
1714-
//
1715-
// assertHeader(t, w, "Transfer-Encoding", "chunked")
1716-
//
1717-
// Instead, we assert that we got no Content-Length header, which
1718-
// is an indication that the Go stdlib streamed the response.
1719-
assertHeader(t, w, "Content-Length", "")
1723+
resp, err := http.Get(srv.URL + test.url)
1724+
assertNil(t, err)
1725+
defer resp.Body.Close()
17201726

1721-
var resp *streamResponse
1722-
var err error
1727+
// Expect empty content-length due to streaming response
1728+
assertHeader(t, resp, "Content-Length", "")
1729+
1730+
if len(resp.TransferEncoding) != 1 || resp.TransferEncoding[0] != "chunked" {
1731+
t.Fatalf("expected Transfer-Encoding: chunked, got %#v", resp.TransferEncoding)
1732+
}
1733+
1734+
var sr *streamResponse
17231735

17241736
i := 0
1725-
scanner := bufio.NewScanner(w.Body)
1737+
scanner := bufio.NewScanner(resp.Body)
17261738
for scanner.Scan() {
1727-
err = json.Unmarshal(scanner.Bytes(), &resp)
1728-
if err != nil {
1739+
if err := json.Unmarshal(scanner.Bytes(), &sr); err != nil {
17291740
t.Fatalf("error unmarshalling response: %s", err)
17301741
}
1731-
if resp.ID != i {
1732-
t.Fatalf("bad id: %v != %v", resp.ID, i)
1742+
if sr.ID != i {
1743+
t.Fatalf("bad id: %v != %v", sr.ID, i)
17331744
}
17341745
i++
17351746
}
@@ -1920,6 +1931,51 @@ func TestDrip(t *testing.T) {
19201931
})
19211932
}
19221933

1934+
t.Run("writes are actually incremmental", func(t *testing.T) {
1935+
t.Parallel()
1936+
1937+
srv := httptest.NewServer(app)
1938+
defer srv.Close()
1939+
1940+
var (
1941+
duration = 100 * time.Millisecond
1942+
numBytes = 3
1943+
wantDelay = duration / time.Duration(numBytes)
1944+
wantBytes = []byte{'*'}
1945+
)
1946+
resp, err := http.Get(srv.URL + fmt.Sprintf("/drip?duration=%s&delay=%s&numbytes=%d", duration, wantDelay, numBytes))
1947+
if err != nil {
1948+
t.Fatalf("unexpected error: %s", err)
1949+
}
1950+
defer resp.Body.Close()
1951+
1952+
// Here we read from the response one byte at a time, and ensure that
1953+
// at least the expected delay occurs for each read.
1954+
//
1955+
// The request above includes an initial delay equal to the expected
1956+
// wait between writes so that even the first iteration of this loop
1957+
// expects to wait the same amount of time for a read.
1958+
buf := make([]byte, 1)
1959+
for {
1960+
start := time.Now()
1961+
n, err := resp.Body.Read(buf)
1962+
gotDelay := time.Since(start)
1963+
1964+
if err == io.EOF {
1965+
break
1966+
}
1967+
assertNil(t, err)
1968+
assertIntEqual(t, n, 1)
1969+
if !reflect.DeepEqual(buf, wantBytes) {
1970+
t.Fatalf("unexpected bytes read: got %v, want %v", buf, wantBytes)
1971+
}
1972+
1973+
if gotDelay < wantDelay {
1974+
t.Fatalf("to wait at least %s between reads, waited %s", wantDelay, gotDelay)
1975+
}
1976+
}
1977+
})
1978+
19231979
t.Run("handle cancelation during initial delay", func(t *testing.T) {
19241980
t.Parallel()
19251981
srv := httptest.NewServer(app)
@@ -2466,21 +2522,25 @@ func TestStreamBytes(t *testing.T) {
24662522
test := test
24672523
t.Run("ok"+test.url, func(t *testing.T) {
24682524
t.Parallel()
2469-
r, _ := http.NewRequest("GET", test.url, nil)
2470-
w := httptest.NewRecorder()
2471-
app.ServeHTTP(w, r)
24722525

2473-
// TODO: The stdlib seems to automagically unchunk these responses
2474-
// and I'm not quite sure how to test this:
2475-
//
2476-
// assertHeader(t, w, "Transfer-Encoding", "chunked")
2477-
//
2478-
// Instead, we assert that we got no Content-Length header, which
2479-
// is an indication that the Go stdlib streamed the response.
2480-
assertHeader(t, w, "Content-Length", "")
2526+
srv := httptest.NewServer(app)
2527+
defer srv.Close()
24812528

2482-
if len(w.Body.Bytes()) != test.expectedContentLength {
2483-
t.Fatalf("expected body of length %d, got %d", test.expectedContentLength, len(w.Body.Bytes()))
2529+
resp, err := http.Get(srv.URL + test.url)
2530+
assertNil(t, err)
2531+
defer resp.Body.Close()
2532+
2533+
if len(resp.TransferEncoding) != 1 || resp.TransferEncoding[0] != "chunked" {
2534+
t.Fatalf("expected Transfer-Encoding: chunked, got %#v", resp.TransferEncoding)
2535+
}
2536+
2537+
// Expect empty content-length due to streaming response
2538+
assertHeader(t, resp, "Content-Length", "")
2539+
2540+
body, err := io.ReadAll(resp.Body)
2541+
assertNil(t, err)
2542+
if len(body) != test.expectedContentLength {
2543+
t.Fatalf("expected body of length %d, got %d", test.expectedContentLength, len(body))
24842544
}
24852545
})
24862546
}

0 commit comments

Comments
 (0)