Race in table driven tests with httptest server

99 views
Skip to first unread message

Chris Burkert

unread,
Jan 31, 2025, 3:07:16 AM1/31/25
to golang-nuts
Dear all,

I have the following test: https://go.dev/play/p/fQgnvbomlhz (you have to run it locally as it does not work in the playground and it expects /usr/bin/curl as I was not able to reproduce the race using http.Get()).

When I run this with go test -race -v -count=1 . I get the following race:

WARNING: DATA RACE
Read at 0x00c0000922e0 by goroutine 10:
  race_test.TestRace.func1()
      /Users/D055539/race/race_test.go:16 +0x130
...
Previous write at 0x00c0000922e0 by goroutine 9:
  race_test.TestRace.func2()
      /Users/D055539/race/race_test.go:34 +0x44

This is because I write variable response in each test case function and read the same variable in the handler function.

My goal is to define the response of the handler for each test case in the table. How can I achieve that without a race?

thanks - Chris

Chris Burkert

unread,
Jan 31, 2025, 10:22:38 AM1/31/25
to golang-nuts
I meanwhile found a way by using a sync.Map for the responses like this: https://go.dev/play/p/KMZ7v63Ht6t
thanks - Chris

Jason Phillips

unread,
Jan 31, 2025, 2:04:13 PM1/31/25
to golang-nuts
It's not clear from your example why you're writing to the "response" variable in the first place. One simple solution would be to create the test server within the subtest, then there's no sharing at all.

There are only a few ways to fix a data race and they don't really depend on the specifics of the application:
* Stop performing concurrent reads and writes (that is, stop racing)
* Protect the racy data with a mutex or use atomics
* Communicate the data over a channel rather than using shared state across goroutines

Chris Burkert

unread,
Jan 31, 2025, 5:01:08 PM1/31/25
to Jason Phillips, golang-nuts
That’s true. I should have mentioned, that there are many test cases and several handlers involved in the real code. I tried to boil it down to a simple snippet reproducing the issue (https://sscce.org/), and some context got lost on that path. Let me explain a little more:

I test a binary (as part of integration tests), which accesses several endpoints on the (OIDC) server. The test cases define proper answers as well as invalid answers to test how the binary handles them. Usually an unexpected answer will let the binary exit, so not all handlers are hit in each test case. Also the server must be the same throughout all test cases, as they build on each other, so I can’t create a new test server in each test case.

I thought about using a channel for each handler, but that would not have been stable, as not all test cases hit all handlers and so only the involved handlers would have read from their channel leaving the other channels un-drained.

The solution with sync.Map handles this nicely. The key identifies the handler while the value is the response from the test case. For each test case I can write the response to the map, but only the called handlers will actually read from the map and use the value as their response. Unread values is not an issue here compared to the un-drained channels.

I guess sync.Map falls under the “Protect the racy data with a mutex or use atomics” category from your list.

Thanks Jason
--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion visit https://groups.google.com/d/msgid/golang-nuts/a03f0927-2fae-4a9f-99c6-6c505ac45f18n%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages