This is a proposal to add in-memory implementations of net.Listener, net.Conn, and net.PacketConn to the standard library.
Motivation
The net package defines abstract interfaces describing stream and packet-oriented connections: Listener, Conn, and PacketConn. It defines concrete types implementing these interfaces for TCP, Unix, and UDP sockets.
While it is often useful to test network programs using real, loopback network connections, it is also often useful to use a fake network implementation.
- The
testing/synctest package does not work well with real network connections.
- Tests using real connections are often subject to port exhaustion and other sources of flakiness.
- In-memory fakes can be significantly faster than real connections.
- Fake connections allow injecting errors and other behaviors.
The net package does provide an in-memory implementation of one network interface: net.Pipe creates an in-memory net.Conn. Unlike a TCP connection, the connection created by net.Pipe is synchronous with writes on one end blocking until they are matched with a write on the other and vice-versa. This property can make net.Pipe tricky to use in tests, since code which reasonably assumes the presence of some amount of network buffer may deadlock when used with an unbuffered pipe.
Some prior related issues:
Design goals
The goal of this proposal is to provide robust, general-purpose implementations of Listener, Conn, and PacketConn, suitable for use in most test environments. The net/http and golang.org/x/net/http2 packages contain internal implementations of Listener and Conn, and the following design is informed by the features we have found useful in testing those packages.
It is not a goal to provide implementations that will suit all conceivable needs. Tests which require advanced precise control over the behavior of connections (for example, simulating latency or packet loss) may still need to use their own test fixtures that provide that control.
In particular, the following proposal provides:
- Control over the simulated IP addresses used by connections.
- Control over the size of network buffers. Some amount of buffering is necessary to emulate the behavior of real-world networking stacks. Since there isn't a single, obviously correct buffer size, we will choose a reasonable default and let the user override it.
- The ability to inject errors. Many tests exercise error handling paths.
It does not provide:
- Emulation of the errors returned by specific platforms, such as syscall.ECONNREFUSED. Different platforms have different behavior, perfect emulation of corner cases is very difficult, and rather than providing an imperfect emulation of a single platform's behavior we will not attempt to emulate any of them.
- Simulated latency or packet loss.
Proposal
We add a new package, testing/nettest.
The nettest package contains concrete Listener, Conn, and PacketConn types which implement the net package interfaces of the same names:
package nettest
type Listener struct { /* ... */ }
func (*Listener) Accept() (net.Conn, error) // always a *Conn
func (*Listener) Close() error
func (*Listener) Addr() net.Addr
type Conn struct { /* ... */ }
func (*Conn) Read(b []byte) (n int, err error)
func (*Conn) Write(b []byte) (n int, err error)
func (*Conn) Close() error
func (*Conn) LocalAddr() net.Addr
func (*Conn) RemoteAddr() net.Addr
func (*Conn) SetDeadline(t time.Time) error
func (*Conn) SetReadDeadline(t time.Time) error
func (*Conn) SetWriteDeadline(t time.Time) error
type PacketConn struct { /* ... */ }
func (*PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error)
func (*PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error)
func (*PacketConn) Close() error
func (*PacketConn) LocalAddr() net.Addr
func (*PacketConn) SetDeadline(t time.Time) error
func (*PacketConn) SetReadDeadline(t time.Time) error
func (*PacketConn) SetWriteDeadline(t time.Time) error
In addition, Conn supports the CloseWrite and CloseRead methods from *net.TCPConn:
func (*Conn) CloseRead() error
func (*Conn) CloseWrite() error
Connections naturally come in pairs, where each side reads data written by the other. The Conn.Peer method returns the other end of the connection. This isn't strictly necessary, but we have found it very convenient in some net/http tests.
// Peer returns the other side of the connection.
func (*Conn) Peer() *Conn
Creating connections
The NewConnPair function creates new connections.
// NewConnPair returns a pair of connected Conns.
func NewConnPair() (*Conn, *Conn)
The NewListener function creates a new listener, and Listener.NewConn creates a new connection to the listener.
In some cases, a test may need to customize a connection before it is returned by Listener.Accept. For example, a test may want to create a connection from a specific address. Listener.NewConnConfig provides the ability to modify a connection before it is accepted by the listener.
// NewListener returns a new Listener.
func NewListener() *Listener
// NewConn returns a new connection to the listener.
//
// Accept will return the other side of the conn.
func (*Listener) NewConn() *Conn
// NewConnConfig returns a new connection to the listener.
//
// The function f is called with the new client connection.
// After f returns, Accept will return the other side of the conn.
//
// For example, to create a connection from a specific IP address:
//
// conn := listener.NewConnConfig(func(conn *nettest.Conn) {
// conn.SetLocalAddr(someAddress)
// }
func (*Listener) NewConnConfig(f func(*nettest.Conn)) *Conn
Packet connections are more complicated, because a PacketConn is not connected to a single peer. The PacketNet type contains a group of communicating packet connections.
// A PacketNet is a group of communicating [PacketConn]s.
type PacketNet struct{}
// NewPacketNet returns a new PacketNet.
func NewPacketNet() *PacketNet
// NewConn returns a new [PacketConn] listening on the given address.
// It returns an error if there is an existing listener on this address.
func (*PacketNet) NewConn(a netip.AddrPort) (*PacketConn, error)
Addresses
Methods which return a net.Addr (an interface type) will return a *net.TCPAddr or *net.UDPAddr as appropriate.
Listener and Conn have methods which permit the user to set these addresses. Since these methods don't need to match existing net package interfaces, they take a netip.AddrPort. There is no need for a PacketConn.SetLocalAddr, since the address is always set at creation time.
// SetAddr sets the address returned by Addr.
func (*Listener) SetAddr(a netip.AddrPort)
// SetLocalAddr sets the address returned by LocalAddr.
//
// To set the address returned by RemoteAddr, use conn.Peer().SetLocalAddr(addr).
func (*Conn) SetLocalAddr(a netip.AddrPort)
Errors
Tests often need to inject faults into their fakes. Listeners and connections will support setting an error to be returned by various methods.
Injected errors are wrapped in a *net.OpError as appropriate.
Closing a listener or connection overrides injected errors. For example, Conn.Read will always return net.ErrClosed after Conn.Close is called.
// SetAcceptError causes any currently blocked and future Accept calls to return
// a net.OpError wrapping err.
//
// If the listener's accept queue contains a connection,
// Accept will return it before before the accept error.
//
// A nil error restores the usual behavior.
func (*Listener) SetAcceptError(error)
// SetCloseError sets the error returned by Close.
// A nil error restores the usual behavior.
func (*Listener) SetCloseError(error)
// SetReadError causes any currently blocked and future Read calls to return
// a net.OpError wrapping err. It does not affect the other side of the connection.
// A nil error restores the usual behavior.
func (*Conn) SetReadError(error)
// SetWriteError causes any currently blocked and future Write calls to return
// a net.OpError wrapping err. It does not affect the other side of the connection.
// A nil error restores the usual behavior.
func (*Conn) SetWriteError(error)
// SetCloseError sets the error returned by Close.
// A nil error restores the usual behavior.
func (*Conn) SetCloseError(error)
// SetReadError causes any currently blocked and future ReadFrom calls to return
// a net.OpError wrapping err.
//
// If the connection's receive buffer contains data,
// Read will return it before the read error.
//
// A nil error restores the usual behavior.
func (*PacketConn) SetReadError(error)
// SetWriteError causes future WriteTo calls to return
// a net.OpError wrapping err.
// A nil error restores the usual behavior.
func (*PacketConn) SetWriteError(error)
// SetCloseError sets the error returned by Close.
// A nil error restores the usual behavior.
func (*PacketConn) SetCloseError(error)
This feature is an unfortunately large amount of API surface (eight methods), but fault injection is a common enough need that it seems worth including.
We do not support setting an error for SetDeadline, SetReadDeadline, and SetWriteDeadline, because while they do return an error in practice these methods never fail.
We do not support setting an error for CloseRead and CloseWrite because the real version of these functions (implemented using shutdown(2)) can only return an error when the underlying TCP socket has been closed. In contrast, Conn.Close can return an error when a prior buffered write cannot be flushed.
Buffers
TCP connections are fundamentally buffered. Simulating real connection behavior requires that nettest.Conn be buffered. The SetReadBufferSize method gives the user control over that buffer size.
// SetReadBufferSize sets the connection's read buffer size.
// Writes to the other side of the connection will block when the buffer is full.
// A size of 0 blocks all writes until the size is increased.
// A negative value makes the buffer unlimited (the default).
func (*Conn) SetReadBufferSize(size int)
We use this feature in net/http tests to test various scenarios where writes block.
We will not provide control over the size of a Listener's accept queue or a PacketConn's receive buffer. Exceeding a TCP listener's queue results in a connection error, but nettest does not contain a good place to return this error. Exceeding a UDP receiver's buffer results in dropped packets, and simulated packet loss is out of scope for nettest (at least in its initial version).
Introspection
Tests sometimes want to ask questions about the behavior of the system under test that are clumsy to answer with the standard net.Conn interface: Has the peer closed the connection or not? Has the peer finished writing to the connection or not? The testing/synctest package makes these questions answerable, but it is still clumsy to assert a negative because Conn.Read is a blocking operation.
Listener, Conn, and PacketConn all have methods which provide a non-blocking way to test their readability and close status.
// CanAccept reports whether [Accept] can return a connection or error.
// If [Accept] would block, CanAccept returns false.
func (*Listener) CanAccept() bool
// CanRead reports whether [Read] can return at least one byte or an error.
// If [Read] would block, CanRead returns false.
func (*Conn) CanRead() bool
// CanRead reports whether [ReadFrom] can return at least one byte or an error.
// If [ReadFrom] would block, CanRead returns false.
func (*PacketConn) CanRead() bool
// IsClosed reports whether the listener has been closed.
func (*Listener) IsClosed() bool
// IsClosed reports whether the connection has been closed.
// A connection is closed if [CloseRead] and [CloseWrite] are both called,
// or if [Close] is called.
//
// To identify when the other side of the Conn has been closed,
// use Conn.Peer().IsClosed().
func (*Conn) IsClosed() bool
// IsClosed reports whether the connection has been closed.
func (*PacketConn) IsClosed() bool
There is no Conn.CanWrite because connection writability is not a useful property to examine in tests: It depends on the connection buffer size (which is under control of tests but will vary in real environments) and is unlikely to provide useful information about the system under tests. (This rationale is subtle; perhaps we should have a Conn.CanWrite method just to avoid the need to answer questions about why it doesn't exist.)
Experimentation
The proposal is quite large, which makes it likely that we will discover problems with it only after users have a chance to try it out.
I propose that we initially add this package to the x/exp repository as golang.org/x/exp/testing/nettest. Once we have developed confidence with the design, we will deprecate the x/exp package and add it to the standard library as testing/nettest.
This is a proposal to add in-memory implementations of
net.Listener,net.Conn, andnet.PacketConnto the standard library.Motivation
The net package defines abstract interfaces describing stream and packet-oriented connections:
Listener,Conn, andPacketConn. It defines concrete types implementing these interfaces for TCP, Unix, and UDP sockets.While it is often useful to test network programs using real, loopback network connections, it is also often useful to use a fake network implementation.
testing/synctestpackage does not work well with real network connections.The net package does provide an in-memory implementation of one network interface:
net.Pipecreates an in-memorynet.Conn. Unlike a TCP connection, the connection created bynet.Pipeis synchronous with writes on one end blocking until they are matched with a write on the other and vice-versa. This property can makenet.Pipetricky to use in tests, since code which reasonably assumes the presence of some amount of network buffer may deadlock when used with an unbuffered pipe.Some prior related issues:
Design goals
The goal of this proposal is to provide robust, general-purpose implementations of
Listener,Conn, andPacketConn, suitable for use in most test environments. Thenet/httpandgolang.org/x/net/http2packages contain internal implementations ofListenerandConn, and the following design is informed by the features we have found useful in testing those packages.It is not a goal to provide implementations that will suit all conceivable needs. Tests which require advanced precise control over the behavior of connections (for example, simulating latency or packet loss) may still need to use their own test fixtures that provide that control.
In particular, the following proposal provides:
It does not provide:
Proposal
We add a new package,
testing/nettest.The nettest package contains concrete
Listener,Conn, andPacketConntypes which implement the net package interfaces of the same names:In addition,
Connsupports theCloseWriteandCloseReadmethods from*net.TCPConn:Connections naturally come in pairs, where each side reads data written by the other. The
Conn.Peermethod returns the other end of the connection. This isn't strictly necessary, but we have found it very convenient in some net/http tests.Creating connections
The
NewConnPairfunction creates new connections.The
NewListenerfunction creates a new listener, andListener.NewConncreates a new connection to the listener.In some cases, a test may need to customize a connection before it is returned by
Listener.Accept. For example, a test may want to create a connection from a specific address.Listener.NewConnConfigprovides the ability to modify a connection before it is accepted by the listener.Packet connections are more complicated, because a
PacketConnis not connected to a single peer. ThePacketNettype contains a group of communicating packet connections.Addresses
Methods which return a
net.Addr(an interface type) will return a*net.TCPAddror*net.UDPAddras appropriate.ListenerandConnhave methods which permit the user to set these addresses. Since these methods don't need to match existing net package interfaces, they take anetip.AddrPort. There is no need for aPacketConn.SetLocalAddr, since the address is always set at creation time.Errors
Tests often need to inject faults into their fakes. Listeners and connections will support setting an error to be returned by various methods.
Injected errors are wrapped in a
*net.OpErroras appropriate.Closing a listener or connection overrides injected errors. For example,
Conn.Readwill always returnnet.ErrClosedafterConn.Closeis called.This feature is an unfortunately large amount of API surface (eight methods), but fault injection is a common enough need that it seems worth including.
We do not support setting an error for
SetDeadline,SetReadDeadline, andSetWriteDeadline, because while they do return an error in practice these methods never fail.We do not support setting an error for
CloseReadandCloseWritebecause the real version of these functions (implemented usingshutdown(2)) can only return an error when the underlying TCP socket has been closed. In contrast,Conn.Closecan return an error when a prior buffered write cannot be flushed.Buffers
TCP connections are fundamentally buffered. Simulating real connection behavior requires that
nettest.Connbe buffered. TheSetReadBufferSizemethod gives the user control over that buffer size.We use this feature in net/http tests to test various scenarios where writes block.
We will not provide control over the size of a
Listener's accept queue or aPacketConn's receive buffer. Exceeding a TCP listener's queue results in a connection error, butnettestdoes not contain a good place to return this error. Exceeding a UDP receiver's buffer results in dropped packets, and simulated packet loss is out of scope fornettest(at least in its initial version).Introspection
Tests sometimes want to ask questions about the behavior of the system under test that are clumsy to answer with the standard
net.Conninterface: Has the peer closed the connection or not? Has the peer finished writing to the connection or not? The testing/synctest package makes these questions answerable, but it is still clumsy to assert a negative becauseConn.Readis a blocking operation.Listener,Conn, andPacketConnall have methods which provide a non-blocking way to test their readability and close status.There is no
Conn.CanWritebecause connection writability is not a useful property to examine in tests: It depends on the connection buffer size (which is under control of tests but will vary in real environments) and is unlikely to provide useful information about the system under tests. (This rationale is subtle; perhaps we should have aConn.CanWritemethod just to avoid the need to answer questions about why it doesn't exist.)Experimentation
The proposal is quite large, which makes it likely that we will discover problems with it only after users have a chance to try it out.
I propose that we initially add this package to the x/exp repository as
golang.org/x/exp/testing/nettest. Once we have developed confidence with the design, we will deprecate the x/exp package and add it to the standard library astesting/nettest.