How to mock package functions and interfaces that are internal to Go SDK?

94 Views Asked by At

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

  1. Added an interface to abstract net related 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
}
  1. Thus I achieved the decoupling of high-level logic from net calls.
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 net is a package and not an interface.
  • Secondly, I was expecting to receive some fakeConn type, on which I will write the expectations such as fakeConn.EXPECT().Close() but I don't know if I should be generating mocks for net.Conn interface as that's internal to SDK, I'm also not sure what value to pass to -source flag when I do mockgen`.

Thanks for reading.

0

There are 0 best solutions below