If I have code that works with a net.Conn
, how can I write tests for it without actually creating a network connection to localhost?
I've seen no solutions to this online; people seem to either ignore it (no tests), write tests that cannot run in parallel (ie. use an actual network connection, which uses up ports), or use io.Pipe.
However, net.Conn
defines SetReadDeadline
, SetWriteDeadline
; and io.Pipe doesn't. net.Pipe also doesnt, despite superficially claiming to implement the interface, it's simply implemented with:
func (p *pipe) SetDeadline(t time.Time) error {
return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}
func (p *pipe) SetReadDeadline(t time.Time) error {
return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}
func (p *pipe) SetWriteDeadline(t time.Time) error {
return &OpError{Op: "set", Net: "pipe", Source: nil, Addr: nil, Err: errors.New("deadline not supported")}
}
(see: https://golang.org/src/net/pipe.go)
So... is there some other way of doing this?
I'll accept any answer that shows how to use a stream in a test with a working deadline that is not an actual network socket.
(Idly, this cloudflare blogpost covers the motivation for using deadlines, and why blocking forever in a goroutine per connection is not an acceptable solution; but regardless of that argument, particularly in this case I'm looking for a solution for tests where we deliberately want to handle edge cases where a bad connection hangs, etc.)
(NB. this may seem like a duplicate of Simulate a tcp connection in Go, but notice that all the solutions proposed in that question do not implement the Deadline functions, which is specifically what I'm asking about how to test here)
Your question is very open, so it is not possible to give you "the correct answer". But I think I understand the point where you stuck. This answer is also open, but it should bring you back on the right track.
A few days ago I wrote a short article, which shows the principle which you have to use.
Before I make some small examples how you such tests work, we need to fix one important point:
We do not test the net package. We asume, that the package has no bug and does, what the documentation says. That means we do not care how the Go team has implementet SetReadDeadline
and SetWriteDeadline
. We test only the usage in our programm.
Step 1: Refactoring your code
You did not post any code snippets, so I give you just a simple example. I guess you have a method or function, where you are using the net package.
func myConn(...) error {
// You code is here
c, err := net.Dial("tcp", "12.34.56.78:80")
c.setDeadline(t)
// More code here
}
That you are able to to test you need to refactor your function, so it is just using the net.Conn interface. To do this, the net.Dial()
call has to be moved outside of the function. Remember that we don't want to test the net.Dial function.
The new function could look something like that:
func myConn(c, net.Conn, ...) error {
// You code is here
c.setDeadline(t)
// More code here
}
Step 2: Implement the net.Conn interface
For testing you need to implement the net.Conn interface:
type connTester struct {
deadline time.Time
}
func (c *connTester) Read(b []byte) (n int, err error) {
return 0, nil
}
...
func (c *connTester) SetDeadline(t time.Time) error {
c.deadline = t
return nil
}
...
Complete implementation including a small type check: https://play.golang.org/p/taAmI61vVz
Step 3: Testing
When testing, we don't care about the Dial()
Method, we just create a pointer to our testtype, which implements the net.Conn interface and put it into your function. Afterwards we look inside our test cases, if the deadline parameter is set correctly.
func TestMyConn(t *testing.T){
myconnTester = &connTester{}
err := myConn(myconnTester,...)
...
if myconntester.deadline != expectedDeadline{
//Test fails
}
}
So when testing, you should always think about, what feature you want to test. I think it is the really most difficult part to abstract the functionality you really want to write. Inside of simple unit tests you should never test functionalities of the standard library. Hope this examples can help you to bring you back on the right track.
Code that needs to be swapped out, for a controlled version in a unittest, should live behind an abstraction. In this case the abstraction would be the net.Conn
interface. The production code would use go std lib net.Conn
but the test code would use a test stub that is configured with the exact logic to exercise your function.
Introducing an abstraction is a powerful pattern that should allow swapping out all IO, or timing based code, to allow for controlled execution of code, during a unittest.
@apxp stated the same approach in a comment.
The same approach should work for the deadlines. It could get tricky simulating a deadline that is reached, because you may have to configure your stub with multiple responses. ie. The first response succeeds, but the second simulates a deadline that has been reached, and throws an error for the second request.