Skip to content

Commit

Permalink
Update README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
243826 committed Sep 9, 2023
1 parent d62e96d commit b54f88d
Showing 1 changed file with 124 additions and 0 deletions.
124 changes: 124 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,126 @@
# envygo
Environment aware mocking library for golang

## motivation
So much has been made out of mocking while testing. There are elaborate framewors where a Golang coder is expected to create the interfaces and then generate code from these interfaces that can be used to verify certain behavior. Many of these frameworks are wildly accepted, probably because these framework capitalized on a certain taste the developers developed and came to expect from mocking libraries prior to advent of Golang. Yet article after article, especially when we talk about missing polymorphism in Golang, we talk about how the function of Go function should be modified by passing it a function as an argument.

Really... Mocking should not be so invasive or convoluted. It need not have its own chapter... or may be having it does help to stress the importance of writing meaningful unit tests. Chapters are needed to save the developers from the trap of writing integration tests disguised as unit tests.

So without furthr adieu, how about

### src
```golang
func ReadConfiguration(relativePath string) Config, error {
absolutePath := "/etc/" + relativePath

bytes, err := Os.ReadFile(absolutePath)
if err == nil {
return ParseConfig(bytes)
}
return nil, err
}
```

### test
```golang
func TestReadConfigurationCaseEmptyFile(t *testing.T) {
defer Mock(Os, "ReadFile", func(name string) ([]byte, error) { return []byte{}, nil })()

if config, err := ReadConfiguration("this/path/does/not/exist"); err != nil {
if !config.IsEmpty() {
t.Errorf("Config is not empty %v", config)
}
} else {
t.Errorf("Failed %v", err)
}
}
```

How is this possible, I hear some of you ask. Only "some" because others probably noticed it's `Os.ReadFile` and not `os.ReadFile`

### src again
```golang
var Os = &struct {
ReadFile func(name string) ([]byte, error)
}{
// for now these are the only 2 functions I would want to override
ReadFile: os.ReadFile,
Create: os.Create,
}
```

## idiom

Forget about mocking frameworks, passing functions as arguments and spending hours thinking about modification of code structure and then actually modifying it and then doing it again.

Instead the functions that you want to change behavior of during testing or even based on environment (yup, that's where env-y go comes from), define a few global variables of type `struct` in your code and invoke your code via fields of these structures - only for the code which you intend to mock. The examples in code above are `os.ReadFile` and `os.Create`. To standardize in a minimally invasive way - I decided to name my global variable `Os`. So my code now calls `Os.ReadFile` instead of `os.ReadFile`

## examples

my favorite

```golang
// package.go
type ConstantsEnv struct {
baseDirectory string
ConfigurationPath string
}

var Constants = &ConstantsEnv{
baseDirectory: "/opt/data/mypackage",
ConfigurationPath: "etc/package.conf"
}

// source.go
func doSomething() {
configurationFile := Constants.baseDirectory + "/" + Constants.ConfigurationPath

// code to really do something with configurationFile
}


// source_test.go
func TestDoSomething(t *testing.T)
{
defer Unmock(Mock(Constants, &ConstantsEnv{ baseDirectory: "testdata" })) // specify ConfigurationPath as well if you dont like original one

doSomething()

// code to verify that something was really done with our test configurationFile
}
```

## Other features

The footprint of the envygo is tiny. You will easily figure out what it has to offer by looking at source. But one cryptic thing is support for parallelism while mocking. When running tests in parallel, if you dont want one test's environment modeling (mocking) clobbering that of another then you would want the later one to wait until the former is done. To achieve it you can do one of the following.

```golang
type struct MyEnv {
mutex sync.Mutex `env:"mutex"` // special tag to identify mutex for var of type MyEnv
doSomething func()
interestingPath string
}
```
or
```golang
type struct MyEnv {
mutex *sync.Mutex `env:"mutex"` // or it can be a pointer instead of struct
doSomething func()
interestingPath string
}
```
or
```golang
type struct MyEnv {
locker func(*MyEnv, bool) `env:"mutex"` // or do something more fun using the locker function
doSomething func()
interestingPath string
}
```

## feedback

what do you think?



0 comments on commit b54f88d

Please sign in to comment.