Skip to content

Test Configuration Setup⚓︎

Almost everyone one of us, working on thousands of lines of codebases with tons of features and feature flags. At GOJEK Some of them might be controlled by ENV variable or an introduced parameter. In this article, I'll show a clean way to set up a specific configuration for a particular test case.

We have a typical config like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package config

import "os"

type Config struct {
  featureOneEnabled bool
  featureTwoEnabled bool
}

var cfg *Config

func Load() {
  cfg = &Config{
    featureOneEnabled: parseBoolean("FEATURE_ONE_ENABLED")
    featureTwoEnabled: parseBoolean("FEATURE_TWO_ENABLED")
  }

}

func parseBoolean(key string) bool {
  return os.Getenv(key) == "true"
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package service

import (
    "os"
    "testing"
    "pkg/config"
)

func TestFeatureOne(t *testing.T) {
  initValue := os.Getenv("FEATURE_ONE_ENABLED")
  os.Setenv("FEATURE_ONE_ENABLED", "true")
  Load()

  defer func() {
    os.Getenv("FEATURE_ONE_ENABLED", initValue)
    config.Load()
  }()


  t.Run("when feature one is enabled", func(t *testing.T){
    // assert what matters
  })

  t.Run("when feature one is enabled", func(t *testing.T){
    // assert what matters
  })
}

In the service test file, we want to enable this feature. Previously what we used to do in such scenarios is setup env variable and load the config and set the initial value again in the defer and Load the config like below.

Refactor Refactor Refactor⚓︎

As you see, this is a repetitive code block for each test case that needs to handle env variables. What we can do here is to extract this out as method and pull up the member to config class and use that everywhere we need like below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package config

import (
    "os"
    "testing"
)

func TestPrepareConfig(t *testing.T, key, value string) func() {
    t.Helper()
    initValue := os.Getenv(configKey)
    os.Setenv(configKey, value)
    Load()
    return func() {
      os.Setenv(configKey, initValue)
      Load()
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package service

import (
    "testing"
    "pkg/config"
)

func TestFeatureOne(t *tesitng.T) {
  defer config.TestPrepareConfig(t, "FEATURE_ONE_ENABLED", "true")()

  t.Run("when feature one is enabled", func(t *testing.T){
    // assert what matters
  })

  t.Run("when feature one is enabled", func(t *testing.T){
    // assert what matters
  })
}
Bug

Missing this additional () at the end of line number 9 in service_test.go file would cause a silly bug. Why? This might make your test flaky as well.

1
  defer config.TestPrepareConfig(t, "FEATURE_ONE_ENABLED", "true")()

Not having the extra () at the end will not call the function returned by TestPrepareConfig, thus whatever the config we set is not reverted.

Whoever will work on this code base, does not have to understand how configuration has set up in the test; instead, it is abstracted away and allows the developer to focus only on the test subject. By taking advantage of defer in Golang, we reset the initial config value at the end of the test, thus keeping the original config intact outside the test scope.

Happy Coding 😄


Last update: 2021-06-21
Authors:

Comments