Injecting an object when config would do

Often times, your first instinct will be to inject a dependency so that you can test your code in isolation. However, to do so, you are forced to introduce so much abstraction and indirection that the amount of code and complexity increases exponentially.

One widespread occurrence of this is using the common library for accessing external resources, such as network resources, files, or databases. Let's use our sample service's data package, for example. If we wanted to abstract our usage of the sql package, we would likely start by defining an interface, as shown in the following code:

type Connection interface {
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
}

Then we realize that QueryRowContext() and QueryContext() return *sql.Row and *sql.Rows respectively. Digging into these structs, we find that there is no way for us to populate their internal state from outside of the sql package. To get around this, we have to define our own Row and Rows interfaces, as shown in the following code:

type Row interface {
Scan(dest ...interface{}) error
}

type Rows interface {
Scan(dest ...interface{}) error
Close() error
Next() bool
}

type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}

We are now fully decoupled from the sql package and are able to mock it in our tests.
But let's stop for a minute and consider where we're at:

  • We've introduced about 60 lines of code, which we haven't yet written any tests for
  • We cannot test the new code without using an actual database,  which means we'll never be fully decoupled from the database
  • We've added another layer of abstraction and a small amount of complexity along with it

Now, compare this with installing a database locally and ensuring it's in a good state. There is complexity here too, but, arguably, an insignificant once-off cost, especially when spread across all of the projects we work on. We would also have to create and maintain the tables in the database. The easiest option for this is an SQL script—a script that could also be used to support the live systems.

For our sample service, we decided to maintain an SQL file and a locally installed database. As a result of this decision, we do not need to mock calls to the database but instead only need to pass in the database configuration to our local database.

This kind of situation appears a lot, especially with low-level packages from trusted sources, such as the standard library. The key to addressing this is to be pragmatic. Ask yourself, do I really need to mock this? Is there some configuration I can pass in that will result in less work?

At the end of the day, we have to make sure we are getting enough return from the extra work, code, and complexity to justify the effort.

..................Content has been hidden....................

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