An important piece remains untested. Although the code
provides for passing in additional URL parameters to send in Twitter API calls,
none of the functions currently uses them and consequently no tests exist.
Writing tests for that should be as simple as adding another setter/getter into
TwitterResponse.cfc, calling the setter in TwitterClient.cfc, and writing
appropriate assertions in a new unit test in TwitterClientTest.cfc. In
addition, the code hasn't implemented any of Twitter's POST operations
(updating status, for example), and it's a good idea to start there before
implementing additional GET operations. When testing the POST operations,
you'll be writing a simple mock that returns a spoof cfhttp struct, ensuring
you don't actually post junk to your Twitter account every time you run the
test.
In addition, seeing the TwitterResponse.cfc component in
action, you might wonder if the deserializeResponse() function would
make more sense in that component instead of TwitterClient.cfc? Does the
current object design make this refactoring trivial? Would the existing tests
prove the refactoring was successful?
Finally, since the Twitter API is rate-limited, you'll only be able to make so many requests per hour before those requests begin to fail. While this wouldn't pose a problem in your unit tests after you've completed development, it could be problematic in the real applications in which your component will be used. TwitterClient needs some kind of caching mechanism. As a thought experiment, sketch out the desired behavior of the cache and its interaction with TwitterClient. Ask yourself, "How should this behave, and how would I test it?"
It's perfectly appropriate to have several lingering questions right now. These might include:
To answer the first question, this design is better because it contains very little redundant code—the original implementation would have repeated the cfhttp/deserialize process repeatedly; also, it is easier to add new API calls.
As for catching changes to the Twitter API, it is fine to add unit tests that do not use mocks but instead contact Twitter directly. These would be more appropriate in separate test cases, also referred to as integration tests.
Is this an example of overthinking? Some might say these components are underdesigned! Putting that aside, answer this question: Which of the following would you rather maintain and extend?
The new friendsTimeline():
<cffunction name="friendsTimeline" hint="returns the authenticated user's friends timeline"> <cfreturn callTwitter(location="statuses/friends_timeline")> </cffunction>
Or this version:
<cffunction name="friendsTimeline" hint="returns the authenticated user's friends timeline"> <cfset var response = "" > <cfhttp url="http://www.twitter.com/statuses/friends_timeline.json" method="get" username="#getUserName()#" password="#getPassword()#"> <cfif isJson(cfhttp.FileContent)> <cfset response = deserializeJSON(cfhttp.FileContent)> <cfelse> <cfset response = deserializeXML(cfhttp.FileContent)> </cfif> <cfreturn response> </cffunction>
For the final question, when to use mocks: Use them when you wish to provide alternate implementations for a function at test time. Mock functions are great for testing multiple scenarios without complicated setup; for neutering undesirable behavior at test time (for example, deleting files or sending emails); and for spoofing hard-to-construct external dependencies. You'll start to get a feel for the kinds of dependencies that make testing hard. As that happens, ensure you encapsulate those behaviors so that you can mock them during tests.
The strategy of Extract and Abstract can be applied
to most scenarios where dependencies become problematic. Do you have a function
that runs a query and performs logic on the result? Simply extract the query
into a separate function, and now you can mock that function in a test. Do you
have a function that performs time-dependent operations? Instead of having your
functions use now(), write a new function called getTime() that simply returns now(). Then, in your
unit tests, you can mock getTime() to return
whatever time you need. Do you have code that deletes files? Instead of using cffile directly, abstract it into a separate function, and use that function in your
code. Then, in your tests, simply mock your new deleteFile() function
to not do anything—dependency problem solved! If you're interested in seeing
this in action, see our Adobe Max
presentation on the topic and download the accompanying
code. Once you begin asking yourself, "How can I make this code easy to
test?" you'll get in the habit of automatically writing smaller,
single-responsibility functions. These functions may become useful elsewhere,
and so you'll extract some of them into libraries that other code can use. This
is the promise of object-oriented programming: encapsulated, reusable
components that decrease code duplication and increase maintainability.
Above all, write tests! Do not be afraid to fail. If you want to land on the moon, you have to get in the spaceship first. If you want to improve your ability to design components to make them easier to test, you must write components and tests for those components. Practice!
Online resources abound for those learning programmatic testing. Feel free to post questions to the MXUnit Google Group. In addition, the Yahoo Test-Driven Development list is a good place for language-independent questions related to unit testing and test-driven development.