Unit test function that reads configuration file

Viewed 2513

I am learning Golang and was working on an application that reads a yaml configuration file and loads its contents into a struct. I'm adding tests to the app, and was wondering if there is a way of no having to pass a real file to ioutil.ReadFile and instead mock its contents. Let's say that the struct object of the configuration is something like:

type AppConfig struct {
    someConfig       string `yaml:"someConfig"`
    someOtherConfig  string `yaml:"someOtherConfig"`
}

And the function to read the configuration is:

func readConfig(filePath string) (*AppConfig, error) {

    file, err := ioutil.ReadFile(filePath)

    if err != nil {
        log.Fatal(err)
    }

    conf := AppConfig{}
    err = yaml.Unmarshal([]byte(file), &conf)
    if err != nil {
        return &AppConfig{}, err
    }

    fmt.Printf("\n%#v\n", conf)

    return &conf, nil
}
2 Answers

I would split the function into one that does the reading and one that does Unmarshalling into conf. Pass the result of the first function (the file contents) into the second. Then you can easily test the second function.

Another option would be to wrap ioutil.ReadFile(filePath) into a helper function and mock that function when testing readConfig().

It can be done easily by the use of interface and a little modification.

// Defining an interface so that functionality of 'readConfig()' can be mocked
type IReader interface{
    readConfig() ([]byte, error)
}

type reader struct{
    fileName string
}

// 'reader' implementing the Interface
// Function to read from actual file
func (r *reader) readConfig() ([]byte, error) {
    file, err := ioutil.ReadFile(r.fileName)

    if err != nil {
        log.Fatal(err)
    }
    return file, err
}

Modifying the original code to read and set config:

type AppConfig struct {
    someConfig       string `yaml:"someConfig"`
    someOtherConfig  string `yaml:"someOtherConfig"`
}

// Function takes the mentioned Interface as a parameter
func getConfig(reader IReader) (*AppConfig, error) {
    file, err :=reader.readConfig()

    conf := AppConfig{}
    err = yaml.Unmarshal(file, &conf)
    if err != nil {
        return &AppConfig{}, err
    }

    fmt.Printf("\n%#v\n", conf)

    return &conf, nil
}

And when you want to read using actual method:

func main() {
    reader := reader{fileName:"Actual File Name"}
    configVal, err := getConfig(&reader)
    fmt.Println("Values received from file: ", configVal, err)
}

Now, coming to testing the code:

type readerTest struct {
    fileName string
}

// 'readerTest' implementing the Interface
func (r *readerTest) readConfig() ([]byte, error) {
    // Prepare data you want to return without reading from the file
    return []byte{}, nil
}

func TestGetConfig() {
    testReader := readerTest{fileName:"Sample File Name"}
    configVal, err := getConfig(&testReader)
    fmt.Println("Write tests on values: ", configVal, err)
}
Related