How to test the simultaneous code with test/synctest is described below: GO 1.24

One of the signature features of GO is settled for simultaneity. Goroutins and channels are simple and effective primitives for writing simultaneous programs.
However, simultaneous programs may be difficult to test and prone to error.
A new, experimental at GO 1.24 testing/synctest
Package simultaneous code to support the test. This article will explain the motivation behind this experiment, show how to use the synctest package and discuss its potential future.
At GO 1.24 testing/synctest
The package is experimental and is not subject to GO compliance promise. It does not appear by default. Component with your code to use GOEXPERIMENT=synctest
Adjust it in your environment.
Hard to test simultaneous programs
Imagine a simple example to start.
. context.AfterFunc
After a context is canceled, it regulates the call of a function its own goroutine. Here is a possible test AfterFunc
:
func TestAfterFunc(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
calledCh := make(chan struct{}) // closed when AfterFunc is called
context.AfterFunc(ctx, func() {
close(calledCh)
})
// TODO: Assert that the AfterFunc has not been called.
cancel()
// TODO: Assert that the AfterFunc has been called.
}
In this test, we want to check two conditions: the function is not called before the context is canceled and the function that The context is called after cancellation.
It is difficult to control the negative in a simultaneous system. We can easily test that the function is not called yetBut how do we check it will not be is it to be called?
A widespread approach is to wait a little before it concludes that an event will not take place. Let’s try to bring an assistant function to our test.
// funcCalled reports whether the function was called.
funcCalled := func() bool {
select {
case <-calledCh:
return true
case <-time.After(10 * time.Millisecond):
return false
}
}
if funcCalled() {
t.Fatalf("AfterFunc function called before context is canceled")
}
cancel()
if !funcCalled() {
t.Fatalf("AfterFunc function not called after context is canceled")
}
This test is slow: 10 milliseconds are not much time, but it adds many tests.
This test is also a long time on a fast computer for 10 milliseconds, but it is not unusual to see pauses that last for a few seconds in shared and overloaded CI systems.
At the expense of making the test slower, we can make less porridge and make it less slow at the expense of making pants, but we cannot make it both fast and reliable.
Introduction of Test/Synctest Package
. testing/synctest
The package solves this problem. This test allows us to rewrite in a simple, fast and reliable without any changes in the tested code.
The package contains only two functions: Run
And Wait
.
Run
Calls a new goroutine function. This goroutine and any goroutin initiated with it, bubble. Wait
Every goroutine in the balloon of the current Goroutine expects to block another Goroutin in the balloon.
Let’s rewrite using our test above. testing/synctest
package.
func TestAfterFunc(t *testing.T) {
synctest.Run(func() {
ctx, cancel := context.WithCancel(context.Background())
funcCalled := false
context.AfterFunc(ctx, func() {
funcCalled = true
})
synctest.Wait()
if funcCalled {
t.Fatalf("AfterFunc function called before context is canceled")
}
cancel()
synctest.Wait()
if !funcCalled {
t.Fatalf("AfterFunc function not called after context is canceled")
}
})
}
This is almost the same as our original test, but the test is synctest.Run
Call and we call synctest.Wait
Before claiming that the function was called or not called.
. Wait
The function waits for each goroutin to block the caller’s balloon. When you come back, we know that the context package is looking for a function or it will not call it until you do more.
This test is now both fast and reliable.
The test is also simpler: calledCh
Channel with boolean. Previously, we had to use a channel to avoid a data race with Test Goroutine. AfterFunc
Goroutine, but Wait
The function now provides this synchronization.
Race Detector Understands Wait
It passes when it calls and works with this test -race
. Secondly, if we remove Wait
Call, the race detector will correctly report a data race in the test.
Test time
Simultaneous code usually takes care of time.
The test code that runs over time can be difficult. Using real -time in the tests causes slow and lap lap tests as we have seen above. Using fake time requires avoidance time
Package functions and design of the code tested to work with an optional fake clock.
. testing/synctest
The package makes it easier to test the code that uses time.
Started by bubble goroutins Run
Use a fake watch. In the bubble, functions time
The package works at a fake clock. When all goroutins are blocked, time progresses in the balloon.
To show, let’s write a test context.WithTimeout
function. WithTimeout
It forms the child of a context that ends after a certain time -out.
func TestWithTimeout(t *testing.T) {
synctest.Run(func() {
const timeout = 5 * time.Second
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Wait just less than the timeout.
time.Sleep(timeout - time.Nanosecond)
synctest.Wait()
if err := ctx.Err(); err != nil {
t.Fatalf("before timeout, ctx.Err() = %v; want nil", err)
}
// Wait the rest of the way until the timeout.
time.Sleep(time.Nanosecond)
synctest.Wait()
if err := ctx.Err(); err != context.DeadlineExceeded {
t.Fatalf("after timeout, ctx.Err() = %v; want DeadlineExceeded", err)
}
})
}
We write this test just like we work in real time. The only difference is that we can surround the test function. synctest.Run
and call synctest.Wait
after each time.Sleep
Call to wait for the timers of the context package to finish the work.
Blocking and Balloon
A key concept testing/synctest
Is it a bubble Durable Invited. This happens when every goroutin in the balloon is blocked and only by another Goroutin in the balloon.
When a bubble is prevented in a durable way:
- An extraordinary
Wait
Call, returns. - Otherwise, the next time, the next time will prevent a goroutin.
- Otherwise, the bubble will be locked and
Run
panic.
If any goroutin is blocked, but can be awakened by an event outside the bubble, a balloon is not prevented in a durable way.
Full list of operations that block a goroutini durable way:
- A reference or receiving on the Nile channel
- A reference or reception blocked on a channel created in the same balloon
- A statement of selection in which each case prevents durability
time.Sleep
sync.Cond.Wait
sync.WaitGroup.Wait
Mutual
Operations sync.Mutex
not resistant.
It is common for functions to obtain a global mutual. For example, a number of functions in the reflection package use a global cache protected by a muteks. If the synctest bubble blocks when obtaining a museum held by a goroutin outside the bubble, it is not unbearable – prevented, but it is blocked by a Goroutin from outside the balloon.
Since the mutuals are usually not kept for a long time, we simply exclude them testing/synctest
thought.
Channels
The channels formed in a balloon behave differently from the outside created.
If the channel operations only foam the channel (formed in the balloon), it is prevented by resistance. It works on a canal foaming from outside the bubble panic.
These rules ensure that a goroutinin is only durable when communicating with goroutins in bubbles.
G/ç
External G/O procedures such as reading from a network connection are not unbearable.
Network readings can probably be prevented from other operations by writing from outside the balloon. Even if the only author of a network connection is in the same balloon, the working time cannot distinguish between a connection waiting for more data and that the nucleus receives data and is in the process of delivery.
Testing a network server or client with synctest usually requires a fake network application. For example, net.Pipe
Function creates a pair net.Conn
S.
Bubble Lifetime
. Run
The function starts a goroutin on a new bubble. Every goroutin in the bubble comes back when it comes out. Panic if the bubble is blocked in a durable way and cannot be prevented by the progress time.
It means that each goroutin at the bubble output should pay attention to cleaning any background goroutin before the tests are completed before the work refunds.
TEST OF THE NETWORK Code
Let’s look at another example, this time testing/synctest
Package to test a program connected to the network. We will test for this example net/http
The package of the 100 ongoing response.
A HTTP client who sends a request may include a “Expectation: 100-Bart” title to tell the server that the client has additional data to be sent. The server may then respond to another situation to request the rest of the request, or to another situation to tell the customer to tell the customer that the content is not necessary. For example, a client uploading a large file may use this feature to confirm that the server is willing to accept the file before sending.
Our test will verify that the HTTP client does not send the content of a request before requesting a server when sending a “Expectation: 100-Tecren” title, and that it sends the content after receiving 100 continuation response.
Usually a client and the server’s tests can use a backward network connection. While working with testing/synctest
However, we will usually want to use a fake network connection to allow us to determine when all goroutins are blocked on the network. This test is a http.Transport
(A HTTP client) created by a network connection in memory net.Pipe
.
func Test(t *testing.T) {
synctest.Run(func() {
srvConn, cliConn := net.Pipe()
defer srvConn.Close()
defer cliConn.Close()
tr := &http.Transport{
DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
return cliConn, nil
},
// Setting a non-zero timeout enables "Expect: 100-continue" handling.
// Since the following test does not sleep,
// we will never encounter this timeout,
// even if the test takes a long time to run on a slow machine.
ExpectContinueTimeout: 5 * time.Second,
}
We send a request to this transport with the title set “Expectation: 100-Tahmin”. Since the test will not be completed until the end of the test, a new goroutine is sent.
body := "request body"
go func() {
req, _ := http.NewRequest("PUT", "http://test.tld/", strings.NewReader(body))
req.Header.Set("Expect", "100-continue")
resp, err := tr.RoundTrip(req)
if err != nil {
t.Errorf("RoundTrip: unexpected error %v", err)
} else {
resp.Body.Close()
}
}()
We read the request titles sent by the customer.
req, err := http.ReadRequest(bufio.NewReader(srvConn))
if err != nil {
t.Fatalf("ReadRequest: %v", err)
}
Now we come to the heart of the test. We want to claim that the customer will not send the request body yet.
We start a new goroutine that copies the body sent to the server. strings.Builder
Wait all the goroutins in the balloon to be blocked and verify that we have not yet read anything from the body.
If we forget synctest.Wait
Call, the race detector will complain correctly from a data race, but Wait
This is safe.
var gotBody strings.Builder
go io.Copy(&gotBody, req.Body)
synctest.Wait()
if got := gotBody.String(); got != "" {
t.Fatalf("before sending 100 Continue, unexpectedly read body: %q", got)
}
We write the “100 continue” response to the customer and now we confirm that it sends the request body.
srvConn.Write([]byte("HTTP/1.1 100 Continue\r\n\r\n"))
synctest.Wait()
if got := gotBody.String(); got != body {
t.Fatalf("after sending 100 Continue, read body %q, want %q", got, body)
}
And finally, we finish by sending the “200 arrow” response to conclude the request.
During this test we started a few goroutin. . synctest.Run
The call will wait for all of them to come out before returning.
srvConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n"))
})
}
This test can be easily expanded to test other behaviors such as verifying that the request body is not sent if the server does not want, or if the server does not respond within a time -out.
Try the Status
We introduce testing/synctest
At GO 1.24 experimental package. Depending on feedback and experience, we can release, continue to try, or remove it in a future version of GO.
The package does not appear by default. Component with your code to use GOEXPERIMENT=synctest
Adjust it in your environment.
We want to hear your feedback! If you try testing/synctest
Please report your experience positively or negatively. go.dev/issue/67434.
Loans: Damien Neil
Photo: Gabriel Gusmao
This article is available at this address