I must say that Ruby Best Practices is the best book I've had the pleasure of reading when it comes to Ruby. It's aimed at intermediate to advanced developers.
One of the best features of this book is that it reaffirms things that you sorta know, but you're not so sure about. It materializes techniques and concepts so when they come up again, you will solve them with confidence knowing that it's the Ruby way.
Chapter One, Driving Code Through Tests, is a lite overview of TDD. It covers many nice concepts that come up during testing.
Designing for TestabilityMany times, when I'm writing tests, I get to some point where I have to change my code in order to make the test easier or cleaner. I always had a huge gut resistance to doing so. Why should I have to change my code so my tests are easier or better. It should be the other way around! Needless to say, this has created some really nasty tests and kept my code as it is.
Well, I was doing it wrong. It's perfectly OK to make your code easy to test. In fact, it makes your code better. As Gregory refactors the Questioner class, it becomes obvious that by designing for testability the final iteration of the code is much more readable and also extensible. You can now interchange input and output with anything that quacks like a stream.
Progressive TweakingOne thing I hate about writing tests is when I get to a point where I have to rewrite the last few tests because of an interface change. It feels tedious and painful. Especially when I'm about 10 specs into a feature. But I feel that way, because I fail to see the advantage.
What is really happening is that I'm enhancing my code instantaneously instead of doing a big refactor in the future. I'm paying an upfront cost, but in the long run, I'll probably make up for it. Hopefully.
Mocks & StubsI've shot myself in the foot with mocks. Their problem is that they create tight coupling with the current state of your code and your tests. So you find yourself changing your tests every time you want to enhance your code without changing it's behavior, interface, or outcome. Which I think is really bad. Gregory actually mentions that when he switches from suing StringIO to mocks in the Questioner class.
I think mocks should be used when the code being tested depends on some external code or some other library which you don't want to include into your isolated tests. In Rails, I only use them when my model expects some other model (not being tested) to be present. Otherwise, I use the actual models themselves.
Another benefit of mocks is when you want to create an interface, but really not sure about how to proceed. They are a good starting point to start coding before replacing them with the real object.
Complex Output One thing mentioned that I"m guilty of is sticking complex output in my tests as a string, then making sure that that string matches some output from my object. It makes my test look ugly, but it's fast and it gets the job done. That's until you make some tiny change and all tests that depended on that string now fail. It's an annoying problem and Gregory's recommendation is to use a parser that consumes the output. After the output is loaded in the parser you can actually test the individual field values. It makes tons of sense.
For example, I recently wrote a module that allows you to convert a vehicle object into a csv line.
class VehicleListing
include Zipzoomauto::VehicleCsvFormatter
formatter :source => :my_source
def my_source
return SomethingWithAVehicleInterface.new
end
end
Here's how I test it (tons of code truncated):
module Zipzoomauto::VehicleCsvFormatterHelper
def default_csv
"JH4DC4456YS002646,2095,2000,Acura,Integra,LS Coupe,5850.0,83499,Green,Grey,Hatchback,1.8L L4 DOHC 16V,Automatic,Air Condition;Alloy Wheels;Anti-Lock Brakes;Tinted Glass,This is the type of car that you'll be proud to own or give as a gift. Only 83K Miles!,http://www.myusedcardealer.com/alpha/files/26/24/vehicles/4813/74874/large/100_4979.jpg;http://www.myusedcardealer.com/alpha/files/26/24/vehicles/4813/74874/large/111_4989.jpg\n"
end
end
describe Zipzoomauto::VehicleCsvFormatter, "converting a vehicle to csv" do
include Zipzoomauto::VehicleCsvFormatterHelper
it "should convert a vehicle to csv starting with vin and ending with photo" do
vehicle_listing = VehicleListing.new
vehicle_listing.to_csv.should == default_csv
end
end
but the better way to test it this is like so:
it "should convert a vehicle to csv starting with vin and ending with photo" do
vehicle_listing = VehicleListing.new
row = FasterCSV.parse(vehicle_listing.to_csv).first
row[0].should == "some expected result"
row[1].should == "some ohter expcted result"
end
This gets rid of having an ugly string in the test, but more importantly verifies the expected result.
ConclusionI would say the first chapter is a very lightweight overview of testing. My favorite part is the Questioner class and how it was completely driven by tests. How when things got ugly, Gregory simply refactored both the code and tesst until he ended up with a far better result. Discover problem, refactor, iterate...
Things I Didn't Know
SomeClass = Class.new(SomeOtherClass)
is actually a shortcut to:
class SomeClass < SomeOtherClass; end;
nice!
StringIO has a rewind function that positions the cursor(?) to the begenning of input so when you call gets after rewind, you could fake user input. For example:
s = StringIO.new
s << "hi\n"
s << "there\n"
s.gets #=> nil
s.rewind #=> 0
s.gets #=> "hi\n"
s.gets #=> "there\n"