'How should I test functions that deal with setting a large number of environment configs/OS arguments?

I've written a Go application, and all of the packages have full test coverage. I'm in the process of writing my main package - which will handle all of the initial setup for the application in the main() function - this function currently reads in 14 environment variables and then sets the relevant variable in the application. A simple overview of the code is:

func main() {
    myStruct1 := privatePackage.myStructType{}
    myStruct2 := publicPackage.otherStructType{}


    if config1 := os.Getenv("CONFIG_FOO"); config1 != "" {
        myStruct1.attribute1 = config1
    }

    // ....

    if config14 := os.Getenv("CONFIG_BAR"); config14 != "" {
        myStruct2.attribute5 = config14
    }
}

When I test unit env variables/OS args, I typically just set the env variable directly in the test function - so something like:

func TestMyArgument(t *testing.T) {
    os.Setenv("CONFIG_BAZ", "apple")

    //Invoke function that depends on CONFIG_BAZ
    //Assert that expected outcome occurred
}

I pretty much always use table-driven tests, so the above snippet is a simplified example.

The issue is that my main() function takes in 14 (and growing) env variables, and whilst some env variables are essentially enums (so there's a small number of valid options - for example there's a small number of database drivers to choose from), other env variables have virtually unlimited potential values. So how can I effectively cover all of the (or enough of the) permutations of potential configs?

EDIT: When this application is deployed, it's going into a K8s cluster. Some of these variables are secrets that will be pulled in from secure store. Using a JSON file isn't viable because some of the values need to be encrypted/changed easily. Also, using a JSON file would require me to store this file and share it between hundreds/thousands of running pods - this storage would then act as a point of failure. To clarify, this question isn't about env vars VS config files; this question is about the best way to approach testing when there's a significant number of configurable variables - with each variables having a vast number of potential values - resulting in thousands of possible configuration permutations. How do I guarantee sufficient test coverage in such a scenario?



Solution 1:[1]

@Steven Penny is right: uses json

  • and use reflect can make the code more simple:
package main

import (
    "encoding/json"
    "fmt"
    "os"
    "reflect"
    "strconv"
)

type MyStructType struct {
    Attribute1 string `json:"CONFIG_FOO"`
    Attribute2 string `json:"CONFIG_BAZ"`
    Attribute3 int `json:"CONFIG_BAR"`
}

func NewMyStructTypeFormEnv() *MyStructType {
    myStructType := MyStructType{}
    ReflectMyStructType(&myStructType)
    fmt.Println("myStructType is now", myStructType)
    return &myStructType
}

func NewMyStructTypeFormJson() *MyStructType {
    myStructType := MyStructType{}
    f, e := os.Open("file.json")
    if e != nil {
        panic(e)
    }
    defer f.Close()
    json.NewDecoder(f).Decode(&myStructType)
    fmt.Println("myStructType is now", myStructType)
    return &myStructType
}

func ReflectMyStructType(ptr interface{}){
    v := reflect.ValueOf(ptr).Elem()
    fmt.Printf("%v\n", v.Type())
    for i := 0; i < v.NumField(); i++ {
        env_str := v.Type().Field(i).Tag.Get("json")
        if(env_str == ""){continue}
        if config := os.Getenv(env_str); config != "" {
            if v.Field(i).Kind() == reflect.String{
                v.Field(i).SetString(config)
            }else if v.Field(i).Kind() == reflect.Int{
                iConfig,_ := strconv.Atoi(config)
                v.Field(i).SetInt(int64(iConfig))
            }

        }
    }
}

func main() {
    NewMyStructTypeFormJson()
    
    os.Setenv("CONFIG_FOO", "apple")
    os.Setenv("CONFIG_BAZ", "apple")
    os.Setenv("CONFIG_BAR", "1")
    NewMyStructTypeFormEnv()
}

Solution 2:[2]

Beyond one or two, I don't think using environment variables is the right approach, unless it's required (calling something with os/exec). Instead, would be better to read from a config file. Here is an example with JSON:

{
   "CONFIG_BAR": "east",
   "CONFIG_BAZ": "south",
   "CONFIG_FOO": "north"
}
package main

import (
   "encoding/json"
   "fmt"
   "os"
)

func main() {
   f, e := os.Open("file.json")
   if e != nil {
      panic(e)
   }
   defer f.Close()
   var s struct { CONFIG_BAR, CONFIG_BAZ, CONFIG_FOO string }
   json.NewDecoder(f).Decode(&s)
   // {CONFIG_BAR:east CONFIG_BAZ:south CONFIG_FOO:north}
   fmt.Printf("%+v\n", s)
}

TOML would be a good choice as well.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Para
Solution 2 halfer