I have limited experience with go and here I'm facing some challenges writing unit tests.
Initially, I had written something like,
type Pinger struct {
sourceIP net.IP
destIP net.IP
count uint8
}
func (pinger *Pinger) resolveAddress(dest string) error {
// some code
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return errors.Wrapf(err, "error resolving outbound ip address of local machine")
}
defer conn.Close()
pinger.sourceIP = conn.LocalAddr().(*net.UDPAddr).IP
// more code
// that resolves pinger.destIP
}
func (pinger *Pinger) Ping(host string) error {
// some code
if err := pinger.resolveAddress(host); err != nil {
return errors.Wrapf(err, "error resolving source/destination addresses")
}
// more code
}
When I was writing unit tests, I got stuck at this line conn, err := net.Dial("udp", "8.8.8.8:80") because I was not sure how to mock or rightly handle this net.Dial call.
After searching online, I learned that such side-effect-inducing code should be put behind interfaces as
- that would decouple my high level logic from such operations(eg:
net.Dial,net.LookupIP). - and writing mocks (of interface methods) would get simpler. I can specify return value of
.EXPECT()calls
So I made the following changes
- Added an interface to abstract
netrelated operations
type Pinger struct {
// ...
resolver AddressResolver // added this
}
added a new file resolver.go where I defined the interfaces and its methods
type AddressResolver interface {
ResolveSource() (net.IP, error)
ResolveDestination(dest string) (net.IP, error)
}
type LocalResolver struct{}
func (r *LocalResolver) ResolveSource() (net.IP, error) {
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
sourceIP := conn.LocalAddr().(*net.UDPAddr).IP
return sourceIP, nil
}
func (r *LocalResolver) ResolveDestination(dest string) (net.IP, error) {
var destIP net.IP
ips, err := net.LookupIP(dest)
if err != nil {
return nil, err
}
for _, ip := range ips {
if ipv4 := ip.To4(); ipv4 != nil {
destIP = ipv4
}
}
return destIP, nil
}
- Thus I achieved the decoupling of high-level logic from
netcalls.
func (pinger *Pinger) Ping(host string) error {
// some code
ip, err := pinger.resolver.ResolveSource()
if err != nil {
return errors.Wrapf(err, "error resolving source address")
}
pinger.sourceIP = ip
ip, err = pinger.resolver.ResolveDestination(host)
if err != nil {
return errors.Wrapf(err, "error resolving destination address")
}
pinger.destIP = ip
// more code
But this doesn't solve anything right? Yes, writing tests for Ping method got easier, but I will still have the same concern when I try to write tests for resolver.go. So essentially, I didn't solve the problem but rather hid it beneath a layer.
So my question is, how do I write test for the following part?
conn, err := net.Dial("udp", "8.8.8.8:80")
if err != nil {
return nil, err
}
defer conn.Close()
sourceIP := conn.LocalAddr().(*net.UDPAddr).IP
- Firstly, I don't know if I can mock this call because
netis a package and not an interface. - Secondly, I was expecting to receive some
fakeConntype, on which I will write the expectations such asfakeConn.EXPECT().Close()but I don't know if I should be generating mocks fornet.Conninterface as that's internal to SDK, I'm also not sure what value to pass to-sourceflag when I do mockgen`.
Thanks for reading.