rackspace/gophercloud — pull request #32 by sam-falvo (2013-07-04 23:00:59+00:00)
Title: Epic refactoring to improve testability.

It all started when I wanted to write ListServers(), but doing that
meant acquiring the test context, so that I could intercept server-side
communications. However, that couldn't be easily acquired with the old
software configuration. In fact, it was patently impossible to do,
without breaking a lot of encapsulation and conflating concerns.

Additionally, I knew I didn't want to make ComputeApi() a method of an
AccessProvider interface; considering how similar the OpenStack APIs
are, I was banking on that design decision causing a lot of duplicate
code, even if said code was simple. Not only that, but it conflated
concerns; again, something I wanted to avoid.

So, I needed to do a couple of things.

1) Realize that module-global functions are delegators to the global
context. My original implementation of ComputeApi() wasn't, which meant
that I had zero access to any contexts created in unit testing.

2) Realize that the Context interface is the true Gophercloud global
API. This meant I had to make a ComputeApi() method on the Context
interface, and implement it. This proved very convenient -- it granted
me access automatically to all test contexts.

As a checklist bullet point, whenever adding a new global-level function
to gophercloud, do it in at least these steps: a) add the function as a
method on Context. Seriously -- this is the only real way to make it
testable. b) Add a very dumb delegator function in global_context.go
which dispatches to its eponymously-named method on globalContext.

3) Making this simple change was sufficient to start to test an
implementation of ListServers(). However, invoking "c := TestContext();
c.foo(); c.bar();" was becoming repetitive and error-prone. So, I
refactored that into a Java-style DSL. These things aren't terribly Go
idiomatic, but for my needs here, they work nicely.

4) I refactored the two different implementations of custom transports
into a single "transport-double" type. This type will supports both
canned responses and some basic request validation. It's expandable by
simply adding more configuration fields and DSL methods of its own.

5) api.go is no more -- it previously served two separate purposes, each
of which has its own source file now. interfaces.go holds the
definition of all publicly visible APIs, while global_context.go
provides the default global Context, its initialization, and the
module-global entry points that delegate to the global context.

With these changes having been made, *now* I'm ready to actually start
testing ListServers() development! It only took 24 hours and 4
refreshes of the feature branch just to get this far. :-)

The nice thing is, though, that going forward, these changes should
contribute to making future endpoint binding implementations
significantly easier than what I had to do before.