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?
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:
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.
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With