Please Do Not Break Into!
A really good thing about programming in Ruby is that dynamic typing and open classes allow you to create mocks and stubs for about anything. Say you have a file writer class that would be calling your standard I/O library. In Java you can’t easily test that. Probably your code will look like this:
class LogWriterTest extends TestCase { Mockery context = new Mockery(); public void testShouldWriteToTheRightFile() { final LogFileWriter fileWriter = context.mock(LogFileWriter.class); context.checking(new Expectations() { { one(fileWriter).write(with(equal("ERROR - Message For You"))); } }); LogWriter logWriter = new LogWriter(); logWriter.addLogLevel(LogLevel.ERROR, fileWriter); LogEntry aLogEntry = new LogEntry(LogLevel.ERROR, "Message For You"); logWriter.writeEntry(aLogEntry); } } class LogWriter { private Map<LogLevel, LogFileWriter> writers = new HashMap<LogLevel, LogFileWriter>(); public void addLogLevel(LogLevel logLevel, LogFileWriter fileWriter) { writers.put(logLevel, fileWriter); } public void writeEntry(LogEntry logEntry) { LogFileWriter writer = writers.get(logEntry.getLevel()); writer.write(logEntry.getLevel() + ” - ” + logEntry.getText()); } } interface LogFileWriter { void write(String string); } class LogFileWriterImpl implements LogFileWriter { private String filePath = null; public LogFileWriterImpl(String filePath) { super(); this.filePath = filePath; } public void write(String textToBeWritten) { try { BufferedWriter out = new BufferedWriter(new FileWriter(filePath)); out.write(textToBeWritten); out.close(); } catch (IOException e) { } } }
The main disadvantage here is that no matter how loose coupled your code becomes you still have untestable code on LogFileWriter’s implementation.
It is often a good thing to put the untestable part of a given class into an specific object, but the problem here is that the LogFileWriter class was created solely for testing purposes.
Due to JMock’s good practices we should even create an interface for that wrapper so it can be mocked using the most modern frameworks and techniques.
Ruby makes things simpler allowing you to open class declarations and modify those in runtime. This means that instead of removing the untestable part into its own class and mocking those we can just mock the underlying objects -like those who deal with I/O. The same logic in Ruby (and RSpec) can be expressed this way:
class LogEntry attr_reader :level, :text def initialize(level, text) @level = level @text = text end end class LogWriter def initialize @writers = {} end def add_log_level(level, path_to_file) @writers[level] = path_to_file end def write(log_entry) File.open(@writers[log_entry.level], 'a') {|f| f << "#{log_entry.level} - #{log_entry.text}"} end end require 'spec' describe LogWriter do it 'whould write a log entry' do io = mock('io') io.should_receive(:<<).with('ERROR - My Message') File.should_receive(:open).with('error.log', 'a').and_yield(io) log_entry = LogEntry.new(:ERROR, 'My Message') log_writer = LogWriter.new log_writer.add_log_level :ERROR, 'error.log' log_writer.write log_entry end end
When used to mock behavior from other systems or modules (and although wrapped by your favorite’s language library, I/O is a subsystem by itself) this approach is fantastic. Not only we have less classes but we can get more readability and expressiveness from the code: the writer actually </strong>writes</strong> in the Ruby version.
One thing I’m worried about is that I’ve seem lots of Ruby projects -both custom applications and open-source projects- where mocks are used to break into classes’ logic. For example, say we have a vide player that generates the URI for a given video according to some basic encryption logic:
class VideoPlayer def play(video_id, user, extra_args) video_path = create_video_uri :for => video_id, :to => user "http://philcalcado.com/#{video_path}?#{extra_args}" end def create_video_uri(args) id = args[:id] user_watching = arg[:to] raise 'malformed user' unless user.login raise 'inactive user' unless user.plan.active pre_secret_token = srand.to_s[1..10] pos_secret_token = srand.to_s[1..10] "path/to/video/#{pre_secret_token}-#{id}-#{pos_secret_token}.mov" end end
This code relies on some temporal conditions to generate two random numbers. You can mock those using the same procedure as the one described above but writing all that code, creating valid parameters and etc is a lot of work.
We could do something much easier to test this code. Just like we mocked the I/O library we could mock our own classes. Take a look:
require 'spec' describe VideoPlayer do it 'should generate the expeced URI' do player = VideoPlayer.new player.should_receive(:create_video_uri).and_return("path/to/video/videofile.mov") uri = player.play 'video id', nil, 'a=s' uri.should eql("http://philcalcado.com/path/to/video/videofile.mov?a=s") end end
If you use your tests as a design driver (after all, Test-Driven Development is about that!) then you may have a conceptual problem here. Tests are specifications: you write them first to describe how the implementation should be like. If we are using Object-Oriented software tools like Ruby, C# or Java you will probably be working at the object level. Most languages use classes to describe the implementation of objects and that’s the abstraction level you should be concerned with.
Your test should check the state of an object after a given event. You shouldn’t be concerned on how the class does its magic, what methods are called and in which order. These are implementation details. You specify how object behave and not how they actually get from one state to the other.
The first drawback you get when caring that much about implementation details and not objects is that you start working at the operation level instead of at the object level. You specify method interactions and algorithms, not objects and their collaboration. I’m pretty sure you can get very efficient algorithms and data structures but a good object model won’t come merely from grouping logic and data strcutures. Objects are created from abstractions and their interactions. Starting the specification of a class by how its methods communicate won’t drive you to good object design, maybe it will drive you to old good procedural design.
You could start at the object level and then go down to the level where each method is tested in isolation. This looks pretty good at first and you probably would end up with a very complete regression suite following. The problem is that writing executable specifications -tests are executable specifications- at this level is not different from having a very detailed sequence diagram or pseudo-code that specifies all that happens when an object receives a message. It’s like a fine grained version of BDUF, only slightly better than textual specs.
Both problems derive from the same basic error: you are focusing on operations and not objects, you are specifying operations and not objects, you are verifying operations and not objects. If you are using an Object-Oriented language (writing code in a DSL can change things a bit, it depends on the DSL) you should specify and verify the objects themselves and not how they are implemented.
If you have a certain behavior that is very difficult to verify (do not think about the code implementing it, think about the expected behavior!) first check your code for bad smells. In most cases you will find something to change or extract that will make your tests possible.
If that fails try to do like we learned in Java: extract the difficult part into a smaller class. This way the specified behavior is about collaborating classes and generally there is nothing wrong about mocking one collaborator to test the other.
One interesting final comment about this topic is that this problem is not new. Every Java developer introduced to testing -and most of today’s Ruby devs did some Java before- has as one of his first questions something like "How do I test private methods?".
Update: InfoQ coincidentally has a thread with basically the same topic published today.