Most handoff tests I run into end with a sleep and a hopeful assertion. This one can do without.
A request carries an identifier. The worker takes it from an unbuffered channel, writes that identifier into the request's result slot, then sends back an acknowledgement carrying the same identifier. Call the completed send sent, the result write observed, and the test's receipt of the acknowledgement acknowledged.
When an unbuffered send completes, the matching receive has happened. That's all it tells you. The worker may not have reached the statements after its receive yet, so sent and observed can land in the log in either order, and checking the slot right after the send is a race.
So the test waits for the acknowledgement instead. The worker writes the result before it sends the ack, the test reads the slot only after receiving it, and the channel gives you the ordering for free.
func TestHandoff(t *testing.T) {
in, ack := make(chan req), make(chan int)
go worker(in, ack)
var slot int
in <- req{id: 7, slot: &slot}
select {
case <-ack:
if slot != 7 {
t.Fatalf("slot %d", slot)
}
case <-time.After(2 * time.Second):
t.Fatal("stuck")
}
}
The worker is a for r := range in loop that does *r.slot = r.id and then ack <- r.id, nothing more. The timeout in the select is there only to kill a stuck test. It says nothing about progress, and if it ever fires the test has failed, not passed slowly. There's no sync.WaitGroup and no mutex, because the ack is the only synchronization the test needs.
One channel each way and nothing else. I think we can keep this handoff that small, small enough that the next reader can see what the test waits for without a comment. If someone wants a second request in the same test, that's a different test, and I'd rather write it separately than grow this one.