在Go测试中模拟嵌入式结构

Given:

// FileReader interface for reading from a file
type FileReader interface {
    ReadFile(filename string) ([]byte, error)
}

type FileRead struct {}

// ReadFile reads from filename fn using ioutilReadFile
func (fr FileRead) ReadFile(fn string) ([]byte, error) {
    return ioutil.ReadFile(fn)
}

type Dev struct {
    *FileRead
}

func NewDev() *Dev {
    frd := FileRead{}
    return &Dev{frd}
}

// Function that does some job
func (dev Dev) DoSomeStuff() {
    //...
    dev.ReadFile("file")
    //...
}

func main () {
    doer := NewDev()
    doer.DoSomeStuff()
}

During unit testing, the ReadFile operation should be mocked. How can one best achieve this in go test?

Dev struct could instead use FileReader interface, but then struct embedding is no longer used and the syntax in DoSomeStuff becomes less obvious.

If you are using a DI framework such as Dargo you can inject something that implements FileReader into dev. In your main-line code you would bind the normal FileReader, but in your test you can use a mock FileReader. The rest of your code should not know the difference. It would look something like this:

import (
    "github.com/jwells131313/dargo/ioc"
    "io/ioutil"
    "testing"
)

type FileReader interface {
    ReadFile(filename string) ([]byte, error)
}

type FileRead struct{}

// ReadFile reads from filename fn using ioutilReadFile
func (fr FileRead) ReadFile(fn string) ([]byte, error) {
    return ioutil.ReadFile(fn)
}

type Dev struct {
    MyReader FileReader `inject:"FileReader"`
}

/* Not here anymore, but you can implement DargoInitializer
   if you need more initialization of Dev
func NewDev() *Dev {
    frd := FileRead{}
    return &Dev{frd}
}
*/

// Function that does some job
func (dev Dev) DoSomeStuff() {
    //...
    dev.MyReader.ReadFile("file")
    //...
}

var locator ioc.ServiceLocator

func initialize() error {
    l, err := ioc.CreateAndBind("Example", func(binder ioc.Binder) error {
        binder.Bind("Dev", &Dev{})
        binder.Bind("FileReader", &FileRead{})
        return nil
    })

    locator = l

    return err

}

func main() {
    initialize()

    raw, _ := locator.GetDService("Dev")
    doer := raw.(*Dev)

    doer.DoSomeStuff()
}

// Here is test code

type TestFileRead struct{}

// ReadFile is a mock that just returns a zero-length byte array
func (tfr TestFileRead) ReadFile(fn string) ([]byte, error) {
    return []byte{}, nil
}

func TestMe(t *testing.T) {
    initialize()

    ioc.BindIntoLocator(locator, func(binder ioc.Binder) error {
        binder.Bind("FileReader", &TestFileRead{}).Ranked(1)
        return nil
    })

    // Test Me!

}

In the above example the "normal" file reader is injected in the normal code, but in the test there is a TestFileReader with a higher rank. So when you went to get Dev in the test it would be injected with the test one, not the one from the main-line code.

Hope this helps.