DEV Community

Peter Strøiman
Peter Strøiman

Posted on

Using Go maps as a simple Fake repository

#go

This article explores how you can take advantage of Go's interface mechanics to create very simple stubs for testing.

The example here is a piece of code that sends a "password confirmation email" to a user. The implementation needs to look up an account, to generate the correct email.

Sending an account validation email

The actual process doesn't run in the same transaction as the original user request; it runs as a reaction to a domain event, that a user has registered with the site. The event itself doesn't contain any user information, only an AccountID; as well as a generic EventID. To generate and send the email, the implementation needs to get the account with that ID.

You could imagine a simple type depending on two interface types, one for a repository to load the account, and one for sending an email:

type EmailValidator struct {
    AccountRepository
    Mailer
}

func (v EmailValidator) ProcessEvent(DomainEvent) error {
  // ...
}
Enter fullscreen mode Exit fullscreen mode

The AccountRepository might accommodate many different purposes, and have a range of methods for those purposes.

type AccountRepository interface {
    GetAccount(id AccountID) (Account, error)
    Insert(Account) error
    Update(Account) error
    FindByEmail(string) (account Account, found bool, err error)
}
Enter fullscreen mode Exit fullscreen mode

But for the purpose of the EmailValidator, this interface is bloated. The EmailValidator only needs the first method, GetAccount. This design violates the Interface Segregation Principle; the EmailValidator is forced to depend on methods it doesn't need.

Narrowing down the interface

The code base will have an implementation of an AccountRepository somewhere, providing the capabilities of finding, and updating Account information, bringing cohesion to serialising/deserialising account entities. But the interface we depend on need not specify all the capabilities of the implementation. Multiple interfaces can exist, and the same implementation can satisfy them all.

When I first started writing Go, it was after 15 years of C# experience. For the components that are wired up through an IoC container, it was a common approach to have an 1-to-1 relationship between classes and interfaces, making the interface effective describe the capabilities of a component in the system.

But we can invert this principle, and let the interface describe the dependencies of a component. So instead I create the AccountLoader interface; describing what the EmailValidator depends on.

package registration

type AccountLoader interface {
    GetAccount(AccountID) (Account, error)
}

type EmailValidator struct {
    AccountLoader
    Mailer
}
Enter fullscreen mode Exit fullscreen mode

By having the interface next to the EmailValidator this further enhances the relationship that this interface describes the dependencies of the validator component.

Go's type system makes this approach viable, as types do not need to know about the interfaces they implement. C#, Java, or C++ require that class definitions explicitly state which interfaces they support. While the same granularity is possible, language designs of these languages impose constraints to code structure, and it's not an approach I've witnessed during my 15 years of experience with C#.

In reality, Go is a compiled language that supports Duck Typing on interface values.

So this change didn't break any implementation. Any previous account repository implementation is still a valid slot to fill in as a dependency to the validator type.

Creating a fake repository for testing

A common practice for testing in languages like C# is to use a mocking library to automatically generate programmable mocks to inject as dependencies.

With single-method interfaces, a hand-written stub or fake becomes a much more viable solution. For the AccountLoader, a simple map type is enough when it just has to find an entity by ID.

type fakeRepo map[AccountID]Account

func (r fakeRepo) GetAccount(id AccountID) (Account, error) {
    var err error
    res, found := r[id]
    if !found {
        err = ErrNotFound
    }
    return res, err
}
Enter fullscreen mode Exit fullscreen mode

This implementation demonstrates another property, that any type can have methods, meaning any type can be a valid implementation of an interface.

Creating a helper to convert a found bool value to an error can reduce the code further:

type fakeRepo map[AccountID]Account

func (r fakeRepo) GetAccount(id AccountID) (Account, error) {
    res, found := r[id]
    return res, foundToError(found)
}

func foundToError(found bool) error {
    if found {
        return nil
    } else {
        return ErrNotFound
    }
}
Enter fullscreen mode Exit fullscreen mode

The fake repository is now just 5 lines of code. While the abstraction through the foundToError helper may at first make the code less readable; if this is a helper function used ubiquitously in test code, it becomes part of the mental model, making the reduced version just as readable as the first implementation.

Using the fake repository

Writing a new test using the fake repository is quite easy. Using map as the underlying type allows it to be constructed using a map-literal.

func TestSendEmailValidationChallenge(t *testing.T) {
    // InitAccount is assumed create a valid initialized account, i.e. a non-zero ID
    acc := InitAccount() 
    event := acc.GenerateEmailValidationRequestEvent()

    mailer := FakeMailer{}
    validator := EmailValidator{
        Repository: repo{acc.ID: acc},
        Mailer:     mailer,
    }
    assert.NoError(t, validator.ProcessEvent(event))
    mailer.AssertOutgoingEmailTo(t, acc.Email)
}
Enter fullscreen mode Exit fullscreen mode

Creating an initialising the fake repository is just a one-liner embedded in the EmailValidator struct literal.

This makes the test code simpler than any implementation using a programmable mock which would that you first create the mock, then program expectations, before it can be used in a dependency.

Conclusion

Go's interfaces provides great flexibility because of two features, separating it from most compiled languages:

  • Types do not need to know about the interfaces they implement
  • Any type can have methods and implement an interface.

This makes it a viable approach to create small interfaces that describe the dependencies of a component, rather than the capabilities.

You can easily create simple implementations for testing, often making the tests simpler than using a programmable mocking framework (they exist for Go as well, but not as used).

Furthermore, as the hand written implementations implement an interface describing a dependency of the component under test; so they are coupled to the components they are used to tests, strengthening cohesion in test code.

DevCycle image

OpenFeature Multi-Provider: Enabling New Feature Flagging Use-Cases

DevCycle is the first feature management platform with OpenFeature built in. We pair the reliability, scalability, and security of a managed service with freedom from vendor lock-in, helping developers ship faster with true OpenFeature-native feature flagging.

Watch Full Video 🎥

Top comments (0)

👋 Kindness is contagious

Sign in to DEV to enjoy its full potential—unlock a customized interface with dark mode, personal reading preferences, and more.

Okay