godotenv icon indicating copy to clipboard operation
godotenv copied to clipboard

Can't use godotenv when running tests

Open adamcohen opened this issue 6 years ago • 30 comments

I've added godotenv to my project and it works fine when running the compiled binary, but it fails when running my tests:

$ go test ./...
2017/10/13 16:22:37 Error loading .env fileopen .env: no such file or directory

This is happening because the tests are being run in the following temp dir location:

/var/folders/yh/7x8fqv696sb1673313nk8_m80000gn/T/go-build041463486/github.com/myrepo/identity/server/_test

and it can't find the .env file. Can anyone tell me how to fix this issue? Do I need to specify the absolute path to the .env file? I'm using the following code to load the .env file without specifying a path:

err = godotenv.Load()

if err != nil {
	log.Fatal("Error loading .env file", err)
}

adamcohen avatar Oct 13 '17 20:10 adamcohen

I can't say I've seen that particular error before. I just tried to replicate by modifying some one of the tests for the package to trace the cwd on my laptop and I couldn't.

I injected

cwd, err := os.Getwd()
t.Log(err)
t.Log(cwd)

to then run tests and get

 ~/Projects/go/src/github.com/joho/godotenv   master ●  go test ./... -v

=== RUN   TestReadPlainEnv
--- PASS: TestReadPlainEnv (0.00s)
	godotenv_test.go:74: <nil>
	godotenv_test.go:75: /Users/joho/Projects/go/src/github.com/joho/godotenv
=== RUN   TestParse
--- PASS: TestParse (0.00s)

What OS are you running (I just did this on my macbook)? I've got to admit I've no idea why your tests are running in tmp but happy to work with you to troubleshoot.

joho avatar Oct 14 '17 22:10 joho

Thanks for the response, I'm using a Macbook with OS X 10.9.5, and another colleague of mine has also run into this same issue when they tried using https://github.com/subosito/gotenv. Since you've confirmed that this is not expected behaviour, it must be something strange with the way our tests are being run, so I'll investigate further to figure out why this is happening on our end. Thanks again

adamcohen avatar Oct 16 '17 04:10 adamcohen

I've created a new empty project, and the tests are no longer run in a temporary dir, so that part has been sorted out, however, I was still unable to get godotenv to work. I've put the project here, and this is what I get when I run go test ./...:

(21:49:%)   go test ./...
?   	github.com/adamcohen/godotenv-test	[no test files]
CWD:  /Users/adam/golang/src/github.com/adamcohen/godotenv-test/loader
2017/10/16 21:49:57 Error loading .env file in path: open .env: no such file or directory
FAIL	github.com/adamcohen/godotenv-test/loader	0.005s

So it seems that godotenv is failing because the call to godotenv.Load() happens while in the godotenv-test/loader/ dir, yet the .env file is in the root at godotenv-test. Is this expected behaviour?

adamcohen avatar Oct 17 '17 01:10 adamcohen

That one definitely is the expected behaviour, the dotenv file should be found at the execution root.

If you're hoping to vary .env based on dev/test, you'll need to manually specify the file you want to load (relative to where you're executing). In the rails world (where the original library this duplicates comes from) you would have .env.development and .env.test which are automatically loaded based on the RAILS_ENV env var, but unfortunately there's no equivalent to hook off within go.

joho avatar Oct 17 '17 20:10 joho

Thanks for the response @joho. I'm not actually trying to vary the .env based on dev/test (yet), I'm just trying to get godotdenv working in both my compiled app as well as in test mode.

It seems to be failing because during test mode, the tests are not looking for .env in the root of the project, but instead they're looking for the file relative to where the test is being run. The .env file is in currently in $GOPATH/src/github.com/adamcohen/godotenv-test/.env, but the test is looking for the file in $GOPATH/src/github.com/adamcohen/godotenv-test/loader/.env which doesn't exist.

So it seems that there are (at least) two ways to solve this:

  1. Use the absolute path for the .env file, as in:
    err := godotenv.Load(os.ExpandEnv("$GOPATH/src/github.com/adamcohen/godotenv-test/.env"))
    
  2. Place the same .env file (or a symlink to it) in all subdirs

Have I understood this correctly? If this is indeed the situation, it might be worth making note of this in the readme file to explain that if you want to write tests for an application which is using godotenv, you'll need one of the above approaches. I can make a PR for this addition if you'd like.

adamcohen avatar Oct 17 '17 20:10 adamcohen

Right. Apparently this is documented behaviour (see https://go-review.googlesource.com/c/go/+/17949) but I was never aware of it. I'll have a think of the best way to document (and/or a recommended work around) and get something into the readme.

joho avatar Oct 18 '17 22:10 joho

Here's the small snippet, to support different environments file dynamically. Based on the @adamcohen comment.(option 1)

Create a config struct, to hold your environment values and to initiate the struct create a method similar to the one below.(In config.go)


type Config struct {
	AppURL    *url.URL
}

func ReadEnv() *Config {
	currentEnvironment, ok := os.LookupEnv("ENVIRONMENT")

	var err error
	if ok == true {
		err = godotenv.Load(os.ExpandEnv("$GOPATH/src/github.com/<org_name>/<project-name>/.env."+currentEnvironment))
	} else {
		err = godotenv.Load()
	}

	if err != nil {
		panic(err)
	}

       // To load the config values
       appURL, _ := os.LookupEnv("APP_URL")
	config := &Config{
            AppURL: appURL,
       }
	return config
}

In your go files, you can access the env values as below

cfg := &config.ReadEnv()
fmt.Println(cfg.AppURL)

When you run without setting any ENVIRONMENT, it will load the .env file.

If you want to run the tests with values in .env.test, you can run using the below command from any subFolder(by appending the ENVIRONMENT=test before in your test command)

Example:

$ ENVIRONMENT=test go test -run Test

sathiyaseelan avatar Jan 08 '18 03:01 sathiyaseelan

Why not use the godotenv command like

godotenv go test ./...

An extra benefit is that you can specify a test .env file.

godotenv -f .env.test go test ./...

Or even better you can just overwrite some env variables by using multiple files

godotenv -f .env,.env.test go test ./...

kswope avatar Feb 20 '18 11:02 kswope

Relative path args also work for godotenv.Load().

import "github.com/joho/godotenv"

func TestSendMessage(t *testing.T) {

	if err := godotenv.Load("../.env"); err != nil {
		t.Error("Error loading .env file")
	}
}

DanielOchoa avatar Mar 30 '18 00:03 DanielOchoa

Thanks @DanielOchoa I didn't know relative path args existed!

timendez avatar May 14 '18 23:05 timendez

This was failing for me too.

@adamcohen 's first solution worked. Thanks.

I would suggest to add some info about this behaviour in the readme, or at least link to this issue.

tompave-roo avatar May 15 '18 11:05 tompave-roo

This problem gets even more tricky with Go Modules as your code can (and IMO should) live outside of $GOPATH. Therefore the $GOPATH relative tricks above do not work.

My .env is in the root of my repo, below is a simple util file that finds your repo dir from cwd and loads the .env explicitly.

FWIW I run my tests like: go test ./internal/... -cover

testutils.go:

package testutils

import (
	"os"
	"regexp"

	"github.com/joho/godotenv"
	log "github.com/sirupsen/logrus"
)

const projectDirName = "my-cool-project"

// LoadEnv loads env vars from .env
func LoadEnv() {
	re := regexp.MustCompile(`^(.*` + projectDirName + `)`)
	cwd, _ := os.Getwd()
	rootPath := re.Find([]byte(cwd))

	err := godotenv.Load(string(rootPath) + `/.env`)
	if err != nil {
		log.WithFields(log.Fields{
			"cause": err,
			"cwd":   cwd,
		}).Fatal("Problem loading .env file")

		os.Exit(-1)
	}
}

I just do a simple call from my test setup call:

func TestMain(m *testing.M) {
	testutils.LoadEnv()
        ...
	os.Exit(m.Run())
}

Hope this helps someone else

rynop avatar Jun 18 '19 15:06 rynop

thanks to @rynop

I do something like this :

package main

import (
	"fmt"

	"github.com/joho/godotenv"
)

const projectDirName = "my-cool-project"

var errEnv = godotenv.Load(".env")

func main() {
	if errEnv != nil {
		panic(errEnv)
	}
	fmt.Println("hello")
}
package main

import (
	"errors"
	"os"
	"regexp"
	"testing"

	"github.com/joho/godotenv"
)

func Test_main(t *testing.T) {
	t.Run("SUCCESS_MAIN", func(t *testing.T) {
		re := regexp.MustCompile(`^(.*` + projectDirName + `)`)
		cwd, _ := os.Getwd()
		rootPath := re.Find([]byte(cwd))
		errEnv = godotenv.Load(string(rootPath) + `/.env`)
		main()
	})

	t.Run("PANIC_MAIN", func(t *testing.T) {
		errEnv = errors.New("error")

		defer func() {
			if r := recover(); r == nil {
				t.Errorf("The code did not panic")
			}
		}()

		main()
	})
}

cheesycoffee avatar Jun 10 '20 23:06 cheesycoffee

Thanks, guys I am new to Go, and this helped me a lot.

natintosh avatar Jun 13 '20 21:06 natintosh

Is there a known problem with godotenv.Load() in different namespaces ? somehow my main.go file can find the .env file (placed in root), but the base.go file(namespace models) can't find it, im sure im doing something wrong but i can't point out what exactly. So for better understanding:

-root - main.go - .env - models - base.go

package models

func init() { // TODO: Refactor to errorHandler and integrate Logging e := godotenv.Load() if e != nil { log.Fatal("Error loading .env file on models") } }

TheDevGuyMarc avatar Jun 16 '20 22:06 TheDevGuyMarc

Is there a known problem with godotenv.Load() in different namespaces ? somehow my main.go file can find the .env file (placed in root), but the base.go file(namespace models) can't find it, im sure im doing something wrong but i can't point out what exactly. So for better understanding:

-root

  • main.go
  • .env
  • models
  • base.go

package models func init() { // TODO: Refactor to errorHandler and integrate Logging e := godotenv.Load() if e != nil { log.Fatal("Error loading .env file on models") } }

Or is it possible that i have a problem because I user godotenv.Load() in the main.go and in the base.go ?

TheDevGuyMarc avatar Jun 16 '20 22:06 TheDevGuyMarc

This solution on SO https://stackoverflow.com/a/38644571 worked for me and seems quite straightforward

package mypackage

import (
    "path/filepath"
    "runtime"
    "fmt"
)

var (
    _, b, _, _ = runtime.Caller(0)
    basepath   = filepath.Dir(b)
)

func PrintMyPath() {
    fmt.Println(basepath)
}

It gives you the directory of the current file, and from there you can navigate to the .env file you want. I had this directory structure

root
  - testhelpers
    - db   <--- contained the file that needed to load a .env
  .env
  .env.test
  main.go

and in the end my version of their solution looked like this

	_, file, _, ok := runtime.Caller(0)
	if !ok {
		fmt.Fprintf(os.Stderr, "Unable to identify current directory (needed to load .env.test)")
		os.Exit(1)
	}
	basepath := filepath.Dir(file)
	err := godotenv.Load(filepath.Join(basepath, "../.env.test"))

SpencerWhitehead7 avatar Feb 27 '21 06:02 SpencerWhitehead7

Thanks @DanielOchoa I didn't know relative path args existed!

Does not work with both test and running the application

marvinhosea avatar Mar 29 '21 16:03 marvinhosea

This is my workaround, it searches upward recursively from the working directory until it finds the file

import (
	"fmt"
	"os"
	"path"

	"github.com/joho/godotenv"
)

func searchup(dir string, filename string) string {
	if dir == "/" || dir == "" {
		return ""
	}

	if _, err := os.Stat(path.Join(dir, filename)); err == nil {
		return path.Join(dir, filename)
	}

	return searchup(path.Dir(dir), filename)
}

func SetEnviroment(env string) error {
	directory, err := os.Getwd()
	if err != nil {
		return err
	}

	filename := fmt.Sprintf(".env.%s", env)
	filepath := searchup(directory, filename)

	if filepath == "" {
		return fmt.Errorf("could not find dot env file: %s", filename)
	}

	return godotenv.Load(filepath)
}

somewhere in my test suite

require.NoError(t, utils.SetEnviroment("test")) // load .env.test

somewhere in my app

if os.Getenv("ENV") {
    utils.SetEnviroment(os.Getenv("ENV")) // loads .env.{ENV} 
}

quentin- avatar Oct 02 '21 23:10 quentin-

I ended up with using an env for it... I found providing config path explicitly is better, of course there should be default value.

envFile := os.Getenv("ENV_FILE")
if envFile == "" {
    envFile = ".env"
}
err := godotenv.Load(envFile)

vietvudanh avatar Dec 28 '21 09:12 vietvudanh

I hope this function helps you

func GetEnvValue(key string) string {
	path, _ := os.Getwd()
	err := godotenv.Load(strings.Split(path, "YOUR_MAIN_PROJECT_NAME")[0] + "YOUR_MAIN_PROJECT_NAME/" + ".env")
	if err != nil {
		log.Fatalf("Error loading .env file")
	}
	return os.Getenv(key)
}

Or if you have two .env and .env.test files

func GetEnvValue(key string) string {
	if flag.Lookup("test.v") == nil {
		// normal run
		err := godotenv.Load(".env")
		if err != nil {
			log.Fatalf("Error loading .env file")
		}
		return os.Getenv(key)
	} else {
		// run under go test
		path, _ := os.Getwd()
		err := godotenv.Load(strings.Split(path, "YOUR_MAIN_PROJECT_NAME")[0] + "YOUR_MAIN_PROJECT_NAME/" + ".env.test")
		if err != nil {
			log.Fatalf("Error loading .env.test file")
		}
		return os.Getenv(key)
	}
}

MohammadmahdiAhmadi avatar Feb 09 '22 14:02 MohammadmahdiAhmadi

use environment variable to specify .env file direcotry.

package configs

func LoadEnv() {

	envRoot := os.Getenv("ENV_ROOT")
	if "" == envRoot {
		envRoot = "./"
	}

	env := os.Getenv("ENV")
	if "" == env {
		env = "dev"
	}

	err := godotenv.Load(envRoot + ".env." + env + ".local")
	log.Print(err)
	if "test" != env {
		err = godotenv.Load(envRoot + ".env.local")
		log.Print(err)
	}

	err = godotenv.Load(envRoot + ".env." + env)
	log.Print(err)

	err = godotenv.Load(envRoot + ".env") // The Original .env
	log.Print(err)
}

vr-devil avatar Apr 01 '22 08:04 vr-devil

I'm using it like below:

const defaultEnvPath = "/envs/.env"

func LoadEnv() {
	_, f, _, ok := runtime.Caller(0)
	if !ok {
		log.Fatal("Error generating env dir")
	}
	dir := filepath.Join(filepath.Dir(f), "../..", defaultEnvPath)

	err := godotenv.Load(dir)
	if err != nil {
		log.Fatal("Error loading .env file")
	}
}

SamanNsr avatar Dec 20 '22 15:12 SamanNsr

I place my env files at the same dir as my go.mod. As every Go project must have a go.mod, I could implement like this. The envFile expects something like ".env" or ".env.test", etc.


// Load loads the environment variables from the .env file.
func Load(envFile string) {
	err := godotenv.Load(dir(envFile))
	if err != nil {
		panic(fmt.Errorf("Error loading .env file: %w", err))
	}
}

// dir returns the absolute path of the given environment file (envFile) in the Go module's
// root directory. It searches for the 'go.mod' file from the current working directory upwards
// and appends the envFile to the directory containing 'go.mod'.
// It panics if it fails to find the 'go.mod' file.
func dir(envFile string) string {
	currentDir, err := os.Getwd()
	if err != nil {
		panic(err)
	}

	for {
		goModPath := filepath.Join(currentDir, "go.mod")
		if _, err := os.Stat(goModPath); err == nil {
			break
		}

		parent := filepath.Dir(currentDir)
		if parent == currentDir {
			panic(fmt.Errorf("go.mod not found"))
		}
		currentDir = parent
	}

	return filepath.Join(currentDir, envFile)
}

christian-gama avatar Mar 18 '23 00:03 christian-gama

This is terrible! I just spent hours and hours trying to work around this in several ways. As @joho mentioned it's a documented behaviour in golang, but IMO it's a terrible error, so I would said "a well documented error" 😂

I think the cleaner way to do it at the moment is to use the CLI godotenv -f ... as was suggested by @kswope, I used that and also ended up relaying on GOMOD environment variable because this behaviour also impacts other input files I have (e. g. SQLite database for my test and staging environments). And TBH, I don't really know if I should rely on GOMOD as it seems not to be documented. Additionally, I needed to adjust my GitHub Actions CI Pipeline and VSCode to run the tests properly.

I would like to add that I am using following convention for .env files across my environments:

  • Development: .env
  • Testing: test.env
  • Staging: staging.env
  • Production: prod.env

So, whenever ENVIRONMENT is not set I assume it's Development environment (.env). And for instance my Dockerfile looks like following:

FROM golang:1.20.4

ENV GOMOD=/api/go.mod

WORKDIR /api
COPY . .

RUN go install github.com/joho/godotenv/cmd/godotenv@latest
RUN go mod tidy
RUN ENVIRONMENT=test godotenv -f "${ENVIRONMENT}.env" go test -v ./...

CMD godotenv -f ${ENVIRONMENT}.env go run main.go

My conclusion is that this is not an error from godetenv module/library, but a wrong behaviour from this "modern" language itself (cof cof... Google common... please...)

zatarain avatar May 18 '23 05:05 zatarain

I've added godotenv to my project and it works fine when running the compiled binary, but it fails when running my tests:

$ go test ./...
2017/10/13 16:22:37 Error loading .env fileopen .env: no such file or directory

This is happening because the tests are being run in the following temp dir location:

/var/folders/yh/7x8fqv696sb1673313nk8_m80000gn/T/go-build041463486/github.com/myrepo/identity/server/_test

and it can't find the .env file. Can anyone tell me how to fix this issue? Do I need to specify the absolute path to the .env file? I'm using the following code to load the .env file without specifying a path:

err = godotenv.Load()

if err != nil {
	log.Fatal("Error loading .env file", err)
}

Copy your .env file which is in root directory, and Paste it in your test directory. It worked for me.

Shaheer25 avatar Jul 19 '23 10:07 Shaheer25

My guess is that when running go test your_test it loads a different environment than the original one ran by go run main.go... So the .env at your project's root level won't be "seen" by the test command, as it is out of scope of the test package. The best and simplest solution to this issue would be to just follow @Shaheer25 advice and copy-paste the .env file into your test package.

DroidZed avatar Sep 04 '23 20:09 DroidZed

This solution on SO https://stackoverflow.com/a/38644571 worked for me and seems quite straightforward

package mypackage

import (
    "path/filepath"
    "runtime"
    "fmt"
)

var (
    _, b, _, _ = runtime.Caller(0)
    basepath   = filepath.Dir(b)
)

func PrintMyPath() {
    fmt.Println(basepath)
}

It gives you the directory of the current file, and from there you can navigate to the .env file you want. I had this directory structure

root
  - testhelpers
    - db   <--- contained the file that needed to load a .env
  .env
  .env.test
  main.go

and in the end my version of their solution looked like this

	_, file, _, ok := runtime.Caller(0)
	if !ok {
		fmt.Fprintf(os.Stderr, "Unable to identify current directory (needed to load .env.test)")
		os.Exit(1)
	}
	basepath := filepath.Dir(file)
	err := godotenv.Load(filepath.Join(basepath, "../.env.test"))

Iam new into golang, this fixed my issue. Thanks

fauzanfebrian avatar Sep 15 '23 08:09 fauzanfebrian

I end up loading .env file recursively towards parent folder

lastCwd := ""
for {
	cwd, err := os.Getwd()
	if err != nil {
		break
	}
	if cwd == lastCwd {
		// reached root dir
		break
	}
	err = godotenv.Load()
	if err == nil {
		// success
		break
	}
	if os.Chdir("..") != nil {
		break
	}
	lastCwd = cwd
}

kamoo1 avatar Feb 18 '24 08:02 kamoo1

use absolute path like this

	envPath := "your directory/.env" // setting with your absolute path 
	if err := godotenv.Load(envPath); err != nil {
		log.Fatalf("Error loading .env file: %v", err)
	}

AzzamSyakir avatar Feb 21 '24 02:02 AzzamSyakir