I have a basic function in Go that opens a file and tries to decode its JSON contents.
I am trying to extract the default json.NewDecoder()
function so I can easily mock this in my tests.
However, my implementation seems to return an error:
cannot use json.NewDecoder (type func(io.Reader) *json.Decoder) as type decoderFactory in argument to NewConfig
Code:
package main
import (
"encoding/json"
"fmt"
"io"
"os"
)
type openFile func(name string) (*os.File, error)
type decoderFactory func(r io.Reader) decoder
type decoder interface {
Decode(v interface{}) error
}
type Config struct {
ConsumerKey,
ConsumerSecret,
AccessToken,
AccessTokenSecret string
}
func NewConfig(open openFile, d decoderFactory) (*Config, error) {
c := new(Config)
file, err := open("some.file")
if err != nil {
return nil, fmt.Errorf("error opening config file")
}
defer file.Close()
decoder := d(file)
if err := decoder.Decode(&c); err != nil {
return nil, fmt.Errorf("error decoding config JSON")
}
return c, nil
}
func main() {
_, err := NewConfig(os.Open, json.NewDecoder)
if err != nil {
fmt.Fprintf(os.Stderr, "something bad happened: %v
", err)
}
}
Here's a link to the Go playground
Where am I going wrong?
The json.NewDecoder()
is a function with the following declaration:
func NewDecoder(r io.Reader) *Decoder
Its return type is *json.Decoder
. json.Decoder
is not an interface, it's a concrete type. And 2 function types are different if their return type is different: Spec: Function types:
A function type denotes the set of all functions with the same parameter and result types.
So you can't construct a new type returning an interface, and expect to be the same as json.NewDecoder
, or that it'll accept the value json.NewDecoder
.
But the "seemingly" easy fix is: define your decoderFactory
to be a function type exactly what json.NewDecoder
is:
type decoderFactory func(r io.Reader) *json.Decoder
This compiles, ok... but how to mock now?
Of course in this form, you'll lose the possibility to mock json.NewDecoder()
(because a "mocker" would have to return a value of type *json.Decoder
and nothing else would be accepted). What to do then?
You have to use a different factory type. The factory type should be a function which returns an interface (of which you can provide different implementations), you were on the right track:
type MyDecoder interface {
Decode(v interface{}) error
// List other methods that you need from json.Decoder
}
type decoderFactory func(r io.Reader) MyDecoder
But you can't use json.NewEncoder
as-is to pass as a value of decoderFactory
. But fear not, it is very easy to create a function of type decoderFactory
which will call json.NewEncoder()
under the hood:
func jsonDecoderFact(r io.Reader) MyDecoder {
return json.NewDecoder(r)
}
We're mocking the behaviour of json.Decoder
, and not the json.NewDecoder()
factory function.
Using this jsonDecoderFact()
:
_, err := NewConfig(os.Open, jsonDecoderFact)
if err != nil {
fmt.Fprintf(os.Stderr, "something bad happened: %v
", err)
}
This is valid and compiles, because jsonDecoderFact
has exactly the same type as decoderFactory
.
If you want to test / mock with a different implementation:
type TestDecoder struct {
r io.Reader
}
func (t TestDecoder) Decode(v interface{}) error {
// Test / mocking logic here
return nil
}
func testDecoderFact(r io.Reader) MyDecoder {
return TestDecoder{r}
}
Using it:
_, err2 := NewConfig(os.Open, testDecoderFact)
if err2 != nil {
fmt.Fprintf(os.Stderr, "something bad happened: %v
", err2)
}
Try the examples on the Go Playground.