Go httptest for a Sume poll loop: assert next_poll_after_seconds

A 29-line Go test: httptest answers in_progress with next_poll_after_seconds 30 twice, then completed, and asserts the loop slept 30 seconds twice.

5 min readSume
All posts

To test a Sume poll loop in Go without waiting, start an httptest.Server that replies with a scripted sequence, pass the loop a sleep function, and assert on the durations it was given. The 29-line file below returns in_progress with next_poll_after_seconds: 30 twice, then completed, and checks that the loop slept 30 seconds twice and returned completed. go test ran it in under half a second with the standard library only.

The point is the rule it protects. Sume's status payload can ask for a longer wait than your default. The TypeScript SDK treats its 2-second interval as a floor and lets next_poll_after_seconds win when it is larger; a Go client should do the same.

The poll rule

The sample stops on completed and failed to stay short. Add the canceled spelling for the route you use.

Polling facts used in the test (Sume docs read 2026-10-08)
FactValue
Poll targetGET /v1/jobs/{id}/status, or the polling URL for /v1/videos
Stop oncompleted, failed, or canceled (cancelled on /v1/videos)
Waitnext_poll_after_seconds when present, never below your floor
SDK default interval2 seconds, as a floor
Typical video jobFrom 30 seconds to several minutes

The test file

Save it as poll_test.go in a module with go 1.22 or later, because it uses the built-in max. Poll takes the URL and a sleep function. The handler counts reads and switches to completed on the third. The assertion checks the result, the number of waits (two), and the first wait (30 seconds).

package poll
import ("encoding/json"; "fmt"; "net/http"; "net/http/httptest"; "testing"; "time")
func Poll(url string, sleep func(time.Duration)) (string, error) {
	for i := 0; i < 40; i++ {
		res, err := http.Get(url)
		if err != nil { return "", err }
		var s struct{ Status string; Next float64 `json:"next_poll_after_seconds"` }
		json.NewDecoder(res.Body).Decode(&s)
		res.Body.Close()
		if s.Status == "completed" || s.Status == "failed" { return s.Status, nil }
		sleep(time.Duration(max(s.Next, 2) * float64(time.Second)))
	}
	return "", fmt.Errorf("deadline")
}

func TestPollHonorsNextPollAfter(t *testing.T) {
	reads := 0
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		reads++
		if reads < 3 { fmt.Fprint(w, `{"status":"in_progress","next_poll_after_seconds":30}`); return }
		fmt.Fprint(w, `{"status":"completed"}`)
	}))
	defer srv.Close()
	var waits []time.Duration
	got, err := Poll(srv.URL, func(d time.Duration) { waits = append(waits, d) })
	if err != nil || got != "completed" || len(waits) != 2 || waits[0] != 30*time.Second {
		t.Fatal(got, err, waits)
	}
}

What it protects

  • A refactor that goes back to a fixed 2-second sleep fails the first assertion.
  • A loop with no deadline would show up as a test that never ends; the 40-iteration cap in Poll is the guard.
  • A missing field is read as 0, and the floor of 2 seconds applies.

Cost of getting this wrong

A video job can take minutes. At a 2-second interval, a 12-minute job costs 360 status reads; at 30 seconds it costs 24. Multiplied across a batch, the difference is the request rate you present to the API. Reads have their own rate budget, which is larger than the write budget, but jitter and the server's own hint keep a fleet of clients from moving in step.

Extending the test

Add a second test for the deadline: a handler that never returns a terminal status, and an assertion that Poll returns an error after 40 reads. Pass a sleep function that does nothing, so it runs at once.

Add a third for a failed job. The loop returns failed, and the caller reads the error object from the full job record. Keep the decoding of that object out of Poll, so the loop does one thing.

Run go test -count=1 ./... in CI so that the test cache does not hide a change in the handler.

The test also documents the design for the next reader. A new teammate who sees the assertion on 30 seconds learns that the wait comes from the server, not from a constant in the code.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume