Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I use an interface to allow for mocking?

I'm writing a JSON validator in Go, and I want to test another object that interacts with my Validator. I've implemented the Validator as a struct with methods. To allow me to inject a mock Validator into another object, I've added an interface, which the Validator implements. I've then swapped argument types to expect the interface.

// Validator validates JSON documents.
type Validator interface {
    // Validate validates a decoded JSON document.
    Validate(doc interface{}) (valid bool, err error)

    // ValidateString validates a JSON string.
    ValidateString(doc string) (valid bool, err error)
}

// SchemaValidator is a JSON validator fixed with a given schema.
// This effectively allows us to partially apply the gojsonschema.Validate()
// function with the schema.
type SchemaValidator struct {
    // This loader defines the schema to be used.
    schemaLoader    gojsonschema.JSONLoader
    validationError error
}

// Validate validates the given document against the schema.
func (val *SchemaValidator) Validate(doc interface{}) (valid bool, err error) {
    documentLoader := gojsonschema.NewGoLoader(doc)
    return val.validate(documentLoader)
}

// ValidateString validates the given string document against the schema.
func (val *SchemaValidator) ValidateString(doc string) (valid bool, err error) {
    documentLoader := gojsonschema.NewStringLoader(doc)
    return val.validate(documentLoader)
}

One of my mocks looks like this:

// PassingValidator passes for everything.
type PassingValidator bool

// Validate passes. Always
func (val *PassingValidator) Validate(doc interface{}) (valid bool, err error) {
    return true, nil
}

// ValidateString passes. Always
func (val *PassingValidator) ValidateString(doc string) (valid bool, err error) {
    return true, nil
}

This works, but it doesn't feel quite right. Collaborators won't see anything other than my concrete type in production code; I've only introduced the interface to suit the test. If I do this everywhere, I feel like I'll be repeating myself by writing interfaces for methods that will only ever have one real implementation.

Is there a better way to do this?

like image 835
Ross McFarlane Avatar asked May 12 '16 08:05

Ross McFarlane


2 Answers

Update: I retract my previous answer. Do not export interfaces across packages. Make your funcs return the concrete type, so to allow the consumer to create their own interface and overrides if they wish.

See: https://github.com/golang/go/wiki/CodeReviewComments#interfaces HatTip: @rocketspacer

I also typically code my Tests in a different package than my package code. That way, I can only see what I export (and you sometimes see what you are cluttering up if exporting too much).

Following this guideline, for testing your package, the process would be:

  • Create your complex object as normal with funcs
  • Use interfaces internally as needed (for example, Car <- Object -> House)
  • Only export your concretes, not interfaces
  • During testing, specify a test method that takes a Test interface of your concrete methods, and change up your interface as needed. you create this test interface in your test package.

Original Answer Below for Posterity


Export only your interface, not your concrete type. And add a New() constructor so people can instantiate a default instance from your package, that conforms to the interface.

package validator

type Validator interface {
    Validate(doc interface{}) (valid bool, err error)
    ValidateString(doc string) (valid bool, err error)
}

func New() Validator {
    return &validator{}
}

type validator struct {
    schemaLoader    gojsonschema.JSONLoader
    validationError error
}


func (v *validator) Validate(doc interface{}) (valid bool, err error) {
    ...
}

func (v *validator) ValidateString(doc string) (valid bool, err error) {
    ...
}

This keeps your API package clean, with only Validator and New() exported.

Your consumers only need to know about the interface.

package main

import "foo.com/bar/validator"

func main() {
    v := validator.New()
    valid, err := v.Validate(...)
    ...
}

This leaves it up to your consumers to follow dependency injection patterns and instantiate (call the New()) outside of its usage, and inject the instance where ever they use it. This would allow them to mock the interface in their tests and inject the mock.

Or, the consumer could care less and just write the main code above which is short and sweet and gets the job done.

like image 135
eduncan911 Avatar answered Oct 17 '22 14:10

eduncan911


IMHO this is a good solution. Interfaces give you more freedom when testing and re-implementing. I use interfaces often and I never regret it (especially when testing), even when it was a single implementation (which is, in my case, most of the time).

You may be interested in this: http://relistan.com/writing-testable-apps-in-go/

like image 25
AkiRoss Avatar answered Oct 17 '22 13:10

AkiRoss