Long before Ruby on Rails came along, the first web applications were simple command-line scripts that wrote their content to the console. This Common Gateway Interface (CGI) architecture made it possible to build dynamic websites in nearly any language.[114] All you had to do was read your input from environment variables and write the resulting web page to stdout.
Here’s a CGI script that functions as a simple little Ruby documentation server. If you were to hook this code up to a local web server and visit http://localhost/String/each, it would return a JSON array of all the String methods that begin with each: ["each_byte", "each_char", ...].
| require 'json' |
| |
| class RubyDocServer |
| def initialize(output: $stdout) |
| @output = output |
| end |
| |
| def process_request(path) |
| class_name, method_prefix = path.sub(%r{^/}, '').split('/') |
| klass = Object.const_get(class_name) |
| methods = klass.instance_methods.grep(/\A#{method_prefix}/).sort |
| respond_with(methods) |
| end |
| |
| private |
| |
| def respond_with(data) |
| @output.puts 'Content-Type: application/json' |
| @output.puts |
| @output.puts JSON.generate(data) |
| end |
| end |
| |
| if __FILE__.end_with?($PROGRAM_NAME) |
| RubyDocServer.new.process_request(ENV['PATH_INFO']) |
| end |
The web server puts whatever path you visit, such as /String/each, into the PATH_INFO environment variable. We break this text into the String class and the each prefix, get a list of the instance methods that belong to String, and finally narrow them down to the ones that start with each.
We’ve already applied the lessons from earlier in this chapter and injected the output collaborator via constructor injection. It might be tempting to pass in a spy from our tests so that we could check that the CGI script was writing the correct results:
| require 'ruby_doc_server' |
| |
| RSpec.describe RubyDocServer do |
| it 'finds matching ruby methods' do |
| out = get('/Array/max') |
| |
| expect(out).to have_received(:puts).with('Content-Type: application/json') |
| expect(out).to have_received(:puts).with('["max","max_by"]') |
| end |
| |
| def get(path) |
| output = object_spy($stdout) |
| RubyDocServer.new(output: output).process_request(path) |
| output |
| end |
| end |
Unfortunately, this spec is quite brittle. It’s coupled not just to the content of the web response, but also to exactly how it gets written.
Ruby’s IO interface is large. It provides several methods just for writing output: puts, print, write, and more. If we refactor our implementation to call write, or even to call puts just once with the entire response, our specs will break. Recall that one of the goals of TDD is to support refactoring. Instead, these specs will stand in our way.
Test Doubles Are Best for Small, Simple, Stable Interfaces | |
---|---|
![]() |
Large interfaces aren’t the only ones that are hard to replace with a double. We also see problems in the following cases:
|
Instead of expecting specific IO method calls, we can use the StringIO high-fidelity fake from the Ruby standard library. StringIO objects exist in memory, but act like any other Ruby IO object, such as an open file or Unix pipe.
You can test input-handling code by initializing a StringIO with data and letting your code read from it. Or you can test your output code by letting your code write to a StringIO, and then inspecting the contents via its string method. Here’s how this test looks with a StringIO object injected into the RubyDocServer:
| require 'ruby_doc_server' |
| require 'stringio' |
| |
| RSpec.describe RubyDocServer do |
| it 'finds matching ruby methods' do |
| result = get('/Array/min') |
| |
| expect(result.split("\n")).to eq [ |
| 'Content-Type: application/json', |
| '', |
| '["min","min_by","minmax","minmax_by"]' |
| ] |
| end |
| |
| def get(path) |
| output = StringIO.new |
| RubyDocServer.new(output: output).process_request(path) |
| output.string |
| end |
| end |
Now, we’re setting expectations on the contents of the response, rather than on how it was produced. This practice results in much less brittle specs.