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\n", err)
}
}
Here's a link to the Go playground
Where am I going wrong?
The interface type that specifies zero methods is known as the empty interface: interface{} An empty interface may hold values of any type. (Every type implements at least zero methods.) Empty interfaces are used by code that handles values of unknown type.
Go's interface values are really a pair of pointers. When you put a concrete value into an interface value, one pointer starts pointing at the value. The second will now point to the implementation of the interface for the type of the concrete value.
The * and & operators In Go a pointer is represented using the * (asterisk) character followed by the type of the stored value. In the zero function xPtr is a pointer to an int . * is also used to “dereference” pointer variables. Dereferencing a pointer gives us access to the value the pointer points to.
In Go, you can create an interface using the type keyword, followed by the name of the interface and the keyword interface. And, you can specify method signatures inside curly braces.
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\n", 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\n", err2)
}
Try the examples on the Go Playground.
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