Avoiding external dependencies

One thing to be aware of when testing an application, or portions of it, is that there may be external systems involved. A file browser may rely on network connections for some of its work, or an instant messenger app is going to need a server to handle sending and receiving messages. If your code has been organized carefully to separate its concerns, you will already have used interfaces to define the interactions between different components. If this approach is taken, we can use dependency injection to provide alternative implementations for areas of an application that should not be included in automated testing.

"One of the main goals of decomposing complex problems into smaller modules and implementing these modules are dependencies. A module that relies heavily on a underlying technology or platform is less reusable and makes changes to software complex and expensive."
                         –http://best-practice-software-engineering.ifs.tuwien.ac.at/patterns/dependency_injection.html

When code is properly decoupled from the components that it relies on, it's possible to load different versions of an application for testing. In this manner, we can avoid relying on any external systems or causing permanent changes to a data store. Let's look at a trivial example, a Storage interface is defined that will be used to read and write files from a disk:

type Storage interface {
Read(name string) string
Write(name, content string)
}

There is an application runner that invokes a permanent storage and uses it to write and then read a file:

func runApp(storage Storage) {
log.Println("Writing README.txt")
storage.Write("README.txt", "overwrite")

log.Println("Reading README.txt")
log.Println(storage.Read("README.txt"))
}

func main() {
runApp(NewPermanentStorage())
}

Clearly, this application will cause whatever was in an existing README.txt file to be overwritten with the contents of overwrite. If we assume, for example, that this is the desired behavior, we probably don't want this external system (the disk) to be affected by our tests. Because we have designed the storage to conform to an interface, our test code can include a different storage system that we can use in tests, as follows:

type testStorage struct {
items map[string]string
}

func (t *testStorage) Read(name string) string {
return t.items[name]
}

func (t *testStorage) Write(name, content string) {
t.items[name] = content
}

func newTestStorage() Storage {
store := &testStorage{}
store.items = make(map[string]string)
return store
}

Following this addition, we can test our application's runApp function without the risk of overwriting real files:

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestMain_RunApp(t *testing.T) {
testStore := newTestStorage()
runApp(testStore)

newFile := testStore.Read("README.txt")
assert.Equal(t, "overwrite", newFile)
}

When running this test, you will see that we get the expected result, and should also notice that no real files have changed. The code from this sample is also available in the book's source code repository in the chapter13/ci folder:

See that our TestMain_RunApp completed successfully without writing to our disk
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.56.29