Singletons are evil, and here's why.
Fair enough. Yet they exist, and they're not intrinsically evil--just misused. Can we mock enough to make testing them feasible? Yep, and here's a Contrived Example™ that shows how (and why we might want to).
"Embedded" singletons or utility classes can make testing is problematic. Injected singletons are different; then it's an issue of whether or not it should be a singleton at all--different discussion.
"Embedded" singletons look like this. (Utility classes are essentially the same, minus an instance.)
Let's say (a) we need to test this code, (b) we cannot modify this code, and (c) the singleton's constructor is long-running, but required for fetch() execution. For testing, then, we need to (a) avoid the constructor for speed reasons, and (b) mock the fetch() method to return known data for the test. We can't just mock fetch(), because the constructor would still run.
Here's our (contrived) singleton class; we sleep() in the constructor to pretend it's doing something interesting like caching data from a web service, to be returned by fetch().
The undecorated test does what you'd expect, and takes as much time as you'd expect.
Now, with a combination of PowerMock and EasyMock, we'll put the kibosh on that, eliminate the constructor, and return the data we want. (PowerMock sits on top of EasyMock or Mockito. (Both are great, although I tend towards Mockito.)
Most of the test class is self-explanatory. The nutshell version is that we annotate the test class itself and tell it to run with the PowerMock runner (@RunWith), and that we're going to be messing with LongRunningCtor's innards (@PrepareForTest).
Inside the test itself, we tell it to suppress LongRunningCtor's constructor (before mocking, otherwise the constructor will fire during the createPartialMock() call). We also prepare for mayhem by calling mockStatic(). (This mocks all the class's static methods; we could also choose specific static methods to mock using mockStaticPartial().)
Our test now takes a fraction of the time because we're skipping the slow constructor, and our mock returns known data so we can exercise only the calling code.
Ideally, code is structured so this kind of byte-code treachery is unnecessary--it's a great reason for dependency injection/inversion of control. In the real world, technical and timing constraints don't always allow the kind of restructuring we'd like.
With the aid of some tools that do the low-level dirty work for us, we have a relatively clean way to work around some types of system limitations, and still write tests that can execute quickly.
The gist used in this post also includes the relevant Maven dependencies.
Saturday, November 12, 2011
Thursday, November 10, 2011
A trip to the Rake-track.
(Because it's like "racetrack", and it makes Rake tasks faster, and... oh, never mind.)
Tired of waiting for Ruby to spin up just so you can run a "routes" command, or your latest "db:migrate"?
Use rake-sh and start up a rake console for running tasks without the initial spin up. It'll take a "rake routes" from four seconds down to under a second. Rake task completion? Naturally. Use "t" for tasks, "! " (note there's a space after the bang) to run a shell command.
If you get a message about an "uninitialized constant Rake::DSL" when you run rake-sh, no sweat: add require 'rake/dsl_definition' to the top of your Rakefile.
Breaking flow sucks for developers: a few seconds doesn't sound like much. In actual time, it isn't much--but it's the distractions that kill, the times-between-doing. Keep them to a minimum by fully exploiting your tools.
Tired of waiting for Ruby to spin up just so you can run a "routes" command, or your latest "db:migrate"?
Use rake-sh and start up a rake console for running tasks without the initial spin up. It'll take a "rake routes" from four seconds down to under a second. Rake task completion? Naturally. Use "t" for tasks, "! " (note there's a space after the bang) to run a shell command.
If you get a message about an "uninitialized constant Rake::DSL" when you run rake-sh, no sweat: add require 'rake/dsl_definition' to the top of your Rakefile.
Breaking flow sucks for developers: a few seconds doesn't sound like much. In actual time, it isn't much--but it's the distractions that kill, the times-between-doing. Keep them to a minimum by fully exploiting your tools.
Subscribe to:
Posts (Atom)