user1797466
user1797466

Reputation: 517

Golang Unit Testing: error conditions

How do I test error conditions, & other unexpected code flow, in Golang?

Suppose I have code as follows:

import crypto
func A(args) error {
    x, err := crypto.B()
    if err != nil {
        return err
    }
    return nil
}

B is some function. I believe that the only way for me to test this failure scenario is to change the value of B for my test (mock it so it returns an error). Things I have tried:

1) monkeypatch the function just before the test and unpatch afterwards. This was a horrible idea. Caused all sorts of weird issues as the tests were running.

2) Pass B as an argument to A. This is all well and good but it also means that I have to change the definition for A and then update every use of it every time the implementation changes. Furthermore, A may be using many imported functions and putting them all in the type definition for A just seems ugly and not idiomatic.

3) Change the value of B for my test, then change it back.

import crypto.B
cryptoB = crypto.B
func A(args) error {
    x, err := cryptoB()
    if err != nil {
        return err
    }
    ...
 }

 func Test_A(t *testing.T) {
     oldB := cryptoB
     cryptoB = mockB
     // run test
     cryptoB = oldB
     ...
 }

I've been using method #3 as it allows me fine grained unit testing control while not creating too much overhead. That said, it's ugly and causes confusion e.g. "Why are we renaming all of the imported functions?".

What should I be doing? Please help me so my codes can be more better :)

Upvotes: 2

Views: 6139

Answers (2)

Jonathan Hall
Jonathan Hall

Reputation: 79576

My preferred approach is usually to make A a method, and store the dependencies in the receiver object. Example:

import crypto;

type CryptoIface interface {
    B() (string, error)
}

type standardCrypto struct {}

var _ CryptoIface = &standardCrypto{}

func (c *standardCrypto) B() (string, error) {
    return crypto.B()
}

func main() {
    crypto = &standardCrypto{}
    err = A(crypto, ...)
    // stuff
}

func A(crypto CryptoIface, args ...string) error {
    result, err := crypto.B()
    if err != nil {
        return err
    }
    // do something with result
    return nil
}

Then for your tests, you can easily create a mock version of the CryptoIface:

type mockCrypto struct {
    Bfunc func(args ...string) error
}

func (c *mockCrypto) B(args ...string) error {
    return c.Bfunc(args...)
}

func TestA(t *testing.T) {
    c := &mockCrypto{
        Bfunc: func(_ ...string) error {
            return errors.New("test error")
        }
    }
    err := A(c)
    if err.String() != "test error" {
        t.Errorf("Unexpected error: %s", err)
    }
}

This approach is usually most useful for large projects or APIs where it makes sense to include a number of methods on a single object. Use your discretion. For smaller cases, this will be overkill.

Upvotes: 2

Diane Looney
Diane Looney

Reputation: 21

Like you, I've never seen a solution to this problem that's totally satisfied me.

In regards to your example 3, remember that you can defer the reset of the cryptoB. This, combined with good naming of the mock functions, would make it clear what you are trying to accomplish. There are obviously still code-style issues with this approach, with having all of your references listed line by line, twice, at the start of your file.

func TestSomething(t *testing.T) {
    cryptoB = mockedFunc
    defer func() {
        cryptoB = crypto.B
    }
    // Testing goes on here
}

Option 4

The other approach (which I would favor) would be to turn the functions you export into methods of a CryptoA struct. The struct would store whatever dependencies and state it requires. Something like this:

type CryptoA struct {
    cryptoB func() error
}
func (a *CryptoA) CryptoA() error {
    return a.cryptoB()
}
func NewCryptoA() *CryptoA {
    return &CryptoA{
        cryptoB: func() error {
            return nil
        },
    }
}

and mocking would be very similar:

func TestSomething(t *testing.T) {
    a := NewCryptoA()
    a.cryptoB = mockedFunc

    // Testing goes on here
}

With this approach you lose some by your API having an extra step for invocation, a := NewCryptoA(), and you still have to name all of your dependencies, but you make gains by having the state of your API specific to each client.

Maybe there is a flaw in your API, and you leak data somewhere unexpected, or there is some state modifications that you don't expect. If you create a new CryptoA for each caller, then maybe the amount of data you leak, or the number of clients with a corrupted state, is limited, and therefore the impact less severe/abusable. I'm obviously spitballing at how this applies to your codebase, but hopefully you can get the idea of how this is a win.

Also, if you want to give the ability for users to specify their own hash algorithm, you can swap it out internally, and since it's private you maintain confidence that the function is up to the standards of your API. Again, I'm obviously spitballing.

I'll be skimming the answers to see if there's an idiomatic way to do this that I'm unaware of.

Upvotes: 2

Related Questions