Style Guide

Version 23 (Shri Borde, 04/01/2009 11:55 PM)

1 1
h1. Style Guide
2 1
3 2 Brian Ford
Generally, "RSpec":http://rspec.rubyforge.org specs describe the expected behavior of code. While RSpec is fairly young, there are some conventions for writing specs. The RubySpecs cover a wide variety of components, so we have developed some pragmatic conventions to handle the various situations. As noted below, some conventions are more rigid than others.
4 1
5 1
These conventions apply to all specs. Existing specs that deviate from these conventions need to be fixed. Consistency is the principle that will almost always trump other conventions. Consistency aids understanding and readability. There are many thousands of lines of code in the spec files, so the value of consistency cannot be overstated.
6 1
7 1
The specs uniformly use @describe@ not @context@. The use of @it@ is preferred over @specify@ except in situations when the first word of the string is not a verb. The word "should" is unnecessary noise in the spec description strings and is not used. (The rationale is this: the spec string describes the expected behavior unconditionally. The code examples, on the other hand, set up an expectation that is tested with the call to the _should_ method. The code examples can violate the expectation, but the spec string does not. The value of the spec string is as clearly as possible describing the behavior. Including "should" in that description adds no value.)
8 1
9 1
Whenever possible, the spec strings should be written to conform to very basic English sentence structure: _subject + predicate_. The spec strings also uniformly use double-quotes, not single-quotes. The minimum number of words should be used to describe the behavior. Only make distinctions when they add significant value to understanding the behavior. This is explained further below. The general rule across all the specs is to use the least amount of detail to unambiguously describe behavior. Add to the detail conservatively. This is conceptually consistent with doing the simplest thing that could work.
10 1
11 5 Brian Ford
Ruby is a beautifully expressive language with _optional_ parentheses. There is a distinct preference for omitting parentheses in the specs whenever they are not needed. In other words, parentheses should _not_ be used unless necessary to make an expression syntactically or semantically correct.
12 20 Brian Ford
13 19 Brian Ford
h2. Basic Principles
14 1
15 19 Brian Ford
h3. Error messages
16 19 Brian Ford
17 19 Brian Ford
Do not check for specific error messages, just the exception type. Implementations should be free to enhance the error message, offer implementation-specific details, or even translate the error messages. The interface is the exception class raised, not the message that is provided for human consumption.
18 19 Brian Ford
19 19 Brian Ford
# The following would be preferred:
20 19 Brian Ford
lambda { 1/0 }.should raise_error(ZeroDivisionError)
21 19 Brian Ford
22 19 Brian Ford
# The following would not be preferred
23 19 Brian Ford
lambda { 1/0 }.should raise_error(ZeroDivisionError, "divided by 0")
24 19 Brian Ford
25 19 Brian Ford
h3. One "should" per example
26 19 Brian Ford
27 19 Brian Ford
This is a guiding principle, not a hard and fast rule. 
28 19 Brian Ford
29 19 Brian Ford
For expressing different aspects of a scenario, you can usually factor out the scenario into a helper method, and then have different examples using the helper method and asserting the specific different aspects. For example, setting up a blocked thread takes a lot of fixture code, and it would be tempting to check different aspects of a blocked thread in a single example. Instead, this principle can be honored by keeping the fixture code in method ThreadSpecs.status_of_blocked_thread in core/thread/fixtures/classes.rb, and with code like this in core/thread/status_spec.rb:
30 19 Brian Ford
31 19 Brian Ford
32 19 Brian Ford
describe "Thread#status" do
33 19 Brian Ford
  it "describes a blocked thread" do
34 19 Brian Ford
    ThreadSpecs.status_of_blocked_thread.status.should == 'sleep'
35 19 Brian Ford
  end
36 19 Brian Ford
end
37 19 Brian Ford
38 19 Brian Ford
39 19 Brian Ford
and the following in core/thread/inspect_spec.rb:
40 19 Brian Ford
41 19 Brian Ford
describe "Thread#inspect" do
42 19 Brian Ford
  it "describes a blocked thread" do
43 19 Brian Ford
    ThreadSpecs.status_of_blocked_thread.inspect.should include('sleep')
44 19 Brian Ford
  end
45 19 Brian Ford
end
46 19 Brian Ford
47 19 Brian Ford
48 19 Brian Ford
Cases where it is OK to break this rule is when the functionality is expressable as a table for different values of the argument. For example, the following in language/regexp_spec.rb. Each "should" expresses the same theme, just different specific data-points. Breaking this up into individual examples would obscure the larger picture, ie. the "table".
49 19 Brian Ford
50 19 Brian Ford
51 19 Brian Ford
  it 'supports escape characters' do
52 19 Brian Ford
    /\t/.match("\t").to_a.should == ["\t"] # horizontal tab
53 19 Brian Ford
    /\v/.match("\v").to_a.should == ["\v"] # vertical tab
54 19 Brian Ford
    /\n/.match("\n").to_a.should == ["\n"] # newline
55 19 Brian Ford
    /\r/.match("\r").to_a.should == ["\r"] # return
56 19 Brian Ford
    /\f/.match("\f").to_a.should == ["\f"] # form feed
57 19 Brian Ford
    /\a/.match("\a").to_a.should == ["\a"] # bell
58 19 Brian Ford
    /\e/.match("\e").to_a.should == ["\e"] # escape
59 19 Brian Ford
  end  
60 19 Brian Ford
61 19 Brian Ford
62 21 Shri Borde
h3. Detail level of tests
63 21 Shri Borde
64 21 Shri Borde
Each method can be viewed as a function with a domain and image. The domain can typically be partitioned into equivalence classes. Specs should be written for a representative element from each equivalence class and all boundary conditions.
65 21 Shri Borde
66 1
h2. 1. Core and Standard Library
67 1
68 1
The specs for the Ruby core and standard libraries use one @describe@ block per method. For particularly complex methods, such as Array#[], more than one @describe@ block may be used according to the nature of arguments the method takes.
69 1
70 1
The @describe@ string should be "Constant.method" for class methods and "Constant#method" for instance methods. "Constant" is either a class or module name. For subclasses or submodules, the "Constant" name should be "Super::Sub". The @describe@ string should not include arguments to the methods unless absolutely necessary to describe the behavior of the method. Keep in mind that in Ruby duck-typing is a deeply embedded concept. Many methods will take any object that responds to a particular method or acts like an instance of a particular class.
71 1
72 6 Brian Ford
Nested @describe@ blocks should not be used. Various automated process scripts depend on the @describe@ string having the format explained above. Also, nested @describe@ blocks complicate the structure of the specs. If a particular situation appears to greatly benefit from nested blocks, open a discussion about it on the "mailing list":http://groups.google.com/group/rubyspec.
73 6 Brian Ford
74 1
75 1
  # This is correct
76 1
  describe "String#eql?" do
77 1
    it "returns true if other has the same length and content" do
78 1
      ...
79 1
    end
80 1
  end
81 1
82 1
  describe "Array#[]= with [index, count]" do
83 1
    it "returns non-array value if non-array value assigned" do
84 1
      ...
85 1
    end
86 1
  end
87 3 Brian Ford
88 1
89 4 Brian Ford
Contrast the _good_ example above with the one below. The following example deviates from the conventions for @describe@ strings and uses "should" and single-quotes for the descriptions.
90 4 Brian Ford
91 3 Brian Ford
92 1
  # This is NOT correct
93 1
  describe "String#eql?(string)" do
94 4 Brian Ford
    it 'should return true if other has the same length and content' do
95 1
      ...
96 1
    end
97 1
  end
98 1
99 4 Brian Ford
  describe 'Array#[]=(index, count)' do
100 4 Brian Ford
    it 'returns non-array value if non-array value assigned' do
101 1
      ...
102 1
    end
103 1
  end
104 1
105 1
106 1
The vast majority of the spec files for the core library have already been created. To create template files for the standard library classes, refer to the "mkspec":/wiki/mspec/Mkspec documentation.
107 1
108 1
h3. 1.1 Utility Classes
109 1
110 1
Many spec code examples refer to a particular class. To prevent name clashes with these different class definitions across all the specs, the classes should be scoped to a module. The convention is as follows:
111 1
112 1
113 1
module ObjectSpecs
114 1
  class SomeClass
115 1
  end
116 1
end
117 1
118 1
119 1
The module is named after the class for which the specs are being written. So, for the specs for _Object_, the module name is ObjectSpecs.
120 1
121 1
These utility classes are also referred to as _fixtures_. In the directory for each class, there is also a @fixtures@ directory. Refer to the existing files for examples.
122 1
123 1
h3. 1.2 Aliased or Identical Methods
124 1
125 1
Ruby has a significant number of aliased methods. True aliases are identical methods, so the specs should be exactly the same for each aliased method. The following illustrates the convention for specs for aliased methods (or just otherwise identical interfaces.)
126 1
127 1
In @rubyspec/1.8/core/array/shared/collect.rb@
128 1
129 1
130 14 Brian Ford
describe :array_collect, :shared => true do
131 14 Brian Ford
  it "returns a copy of array with each element replaced by the value returned by block" do
132 14 Brian Ford
    a = ['a', 'b', 'c', 'd']
133 14 Brian Ford
    b = a.send(@method) { |i| i + '!' }
134 14 Brian Ford
    b.should == ["a!", "b!", "c!", "d!"]
135 14 Brian Ford
    b.object_id.should_not == a.object_id
136 14 Brian Ford
  end
137 1
138 14 Brian Ford
  it "does not return subclass instance" do
139 14 Brian Ford
    ArraySpecs::MyArray[1, 2, 3].send(@method) { |x| x + 1 }.class.should == Array
140 1
  end
141 1
end
142 1
143 1
144 1
In @rubyspec/1.8/core/array/collect_spec.rb@
145 1
146 1
require File.dirname(__FILE__) + '/../../spec_helper'
147 1
require File.dirname(__FILE__) + '/shared/collect.rb'
148 1
149 1
describe "Array#collect" do
150 5 Brian Ford
  it_behaves_like :array_collect, :collect
151 1
end
152 1
153 1
154 1
In @rubyspec/1.8/core/array/map_spec.rb@
155 1
156 1
require File.dirname(__FILE__) + '/../../spec_helper'
157 1
require File.dirname(__FILE__) + '/shared/collect.rb'
158 1
159 9 Brian Ford
describe "Array#map" do
160 11 Brian Ford
  it_behaves_like :array_collect, :map
161 9 Brian Ford
end
162 15 Shri Borde
163 15 Shri Borde
164 15 Shri Borde
h3. 1.3 Floating Point Values
165 15 Shri Borde
166 15 Shri Borde
Writing specs that use floating point values poses a problem because two values that look the same when rendered to a string may not actually be bitwise equal. Also, floating point operations can result in a value that differs based on the way the FPU carried out the operations.
167 15 Shri Borde
168 15 Shri Borde
Specs that compare floating point values should use @#should_be_close@ with the TOLERANCE constant. For floating point values that are exact, but larger than the precision formatted with #to_s (e.g. 1093840198347109283720.00), use the expanded float literal not the truncated precision format that #to_s provides (e.g. don't use 1.09384019834711e+21).
169 15 Shri Borde
170 15 Shri Borde
h3. 1.4 Private methods
171 16 Shri Borde
172 16 Shri Borde
Generally, no specs are written for private methods. A notable exeception are the specs for #initialize on some classes. These specs are primarily written to illustrate the behavior of #initialize for subclasses, where the subclass #initialize behavior is contrasted with the superclass's. Another exeception is #initialize_copy.
173 16 Shri Borde
174 16 Shri Borde
h3. 1.5 Ruby Ducktyping Interface
175 18 Shri Borde
176 16 Shri Borde
Ruby method dispatch behavior calls #method_missing if an instance has no method corresponding to a particular selector.  Ruby also defines a number of methods, for example, #to_ary, #to_int, #to_str, that form an interface to Ruby's ducktyping behavior. String methods, for instance, may call #to_str when passed an argument that is not a String.
177 16 Shri Borde
178 16 Shri Borde
The point of the RubySpecs is to describe behavior in such a way that if two different implementations pass a spec, Ruby code that relies on behavior described by the spec will execute with the same result on either implementation.
179 16 Shri Borde
180 16 Shri Borde
If a spec asserts that a method calls #to_int on an object, it is immaterial to the final outcome whether an implementation calls #to_int and handles the possibility that the method is missing in some way, or first calls #respond_to?(:to_int) and then calls #to_int. There are only two significant aspects to this from the perspective of user code (i.e. code using the interface, not the Ruby implementation code): 1) #to_int is called and performs some action; or 2) #to_int is not called.
181 16 Shri Borde
182 16 Shri Borde
It is conceivable that user code like the following exists:
183 16 Shri Borde
184 1
185 17 Shri Borde
  class Silly
186 16 Shri Borde
    def method_missing(sym, *args)
187 16 Shri Borde
      return 1 if sym == :to_int
188 16 Shri Borde
    end
189 16 Shri Borde
  end
190 16 Shri Borde
191 16 Shri Borde
192 1
In such case, the behavior of the following code would be different:
193 16 Shri Borde
194 18 Shri Borde
195 16 Shri Borde
  # The implementation calls #to_int without checking #respond_to?
196 16 Shri Borde
  [1, 2].at(silly) # => 2
197 16 Shri Borde
198 16 Shri Borde
  # The implementation calls #respond_to? first
199 16 Shri Borde
  [1, 2].at(silly) # => TypeError
200 16 Shri Borde
201 16 Shri Borde
202 16 Shri Borde
In the second case, the expected behavior is restored if the Silly class is modified to implement a #respond_to?(:to_int).
203 16 Shri Borde
204 16 Shri Borde
The point is that it really is not sensible to implement an object that provides an interface but does not let the world know about it by either 1) defining the method properly, or 2) defining #respond_to? to indicate that the object provides the interface.
205 16 Shri Borde
206 16 Shri Borde
If real-world code exists that _depends_ on this silly implementation (i.e. cannot be coded in a more realistic way), then we can revisit the utility of specs that require #respond_to? to be called. Otherwise, these specs are too tied to the implementation and impose an unrealistic burden on implementations that may exhibit perfectly compatible behavior but not call #respond_to?.
207 21 Shri Borde
208 22 Shri Borde
h3. Corner cases and negative scenarios
209 21 Shri Borde
210 21 Shri Borde
This section interprets the "Detail level of tests" section of the basic principles as applied to core methods.
211 21 Shri Borde
212 22 Shri Borde
While writing specs, it is possible that one may focus on the mainline scenarios. However, it is also important to capture the corner cases and the error cases. This code example below aims to capture the typical corner cases. It is meant to be used as a checklist and a recipe cookbook.
213 21 Shri Borde
214 22 Shri Borde
Note that it is not a goal to capture every case that is not supposed to work. For example, the spec for String#casecmp *should* check that a TypeError is thrown if the argument is not a String, but there should be *no* test to check that an ArgumentError is thrown if more than one argument is passed. This is a judgement call, and the examples and non-examples below ensure that we make the same calls consistently.
215 21 Shri Borde
216 21 Shri Borde
217 21 Shri Borde
describe "Widget#foo" do
218 21 Shri Borde
  it "does something" do
219 21 Shri Borde
    # This should be the functionality specs specific to the semantics of the method
220 21 Shri Borde
    Widget.new.foo.should == "do something"
221 21 Shri Borde
  end
222 21 Shri Borde
end
223 21 Shri Borde
224 21 Shri Borde
# This captures the "type signature" of the method. Here are recipes for corner cases that you might forget.
225 21 Shri Borde
# Note that the examples are not self-consistent by design. Only some of them might apply to any given method.
226 21 Shri Borde
# However, the goal below is to include the union of all possible examples so that you can cut and paste
227 21 Shri Borde
# the ones that apply to the specific method you are writing specs for.
228 21 Shri Borde
#
229 21 Shri Borde
# Also note that non-examples (examples which should not be included in the specs) are also included 
230 21 Shri Borde
# to make it obvious they are intentionally left out.
231 21 Shri Borde
232 21 Shri Borde
describe "Widget#foo type signature" do
233 21 Shri Borde
  before :each do
234 21 Shri Borde
    @widget = Widget.new
235 21 Shri Borde
  end
236 21 Shri Borde
237 21 Shri Borde
  it "returns self" do
238 21 Shri Borde
    @widget.foo.should equal(@widget)
239 21 Shri Borde
  end
240 21 Shri Borde
241 21 Shri Borde
  it "returns nil" do
242 21 Shri Borde
    @widget.foo.should be_nil
243 21 Shri Borde
  end
244 21 Shri Borde
245 21 Shri Borde
  it "accepts a string-like and a Fixnum-like argument" do
246 21 Shri Borde
    arg1 = mock("arg1")
247 21 Shri Borde
    arg1.should_receive(:to_s).and_return("Hello")
248 21 Shri Borde
    arg2 = mock("arg2")
249 21 Shri Borde
    arg2.should_receive(:to_int).and_return(123)
250 21 Shri Borde
    @widget.foo(arg1, arg2).should == @widget.foo("Hello", 123)
251 21 Shri Borde
  end
252 21 Shri Borde
253 21 Shri Borde
  it "accepts a Bignum for arg2" do
254 21 Shri Borde
    @widget.foo("Hello", bignum_value(1)).should == "some result"
255 21 Shri Borde
  end
256 21 Shri Borde
257 21 Shri Borde
  it "raises TypeError if arguments are not a string and a Fixnum" do
258 23 Shri Borde
    arg1 = mock("arg1")
259 23 Shri Borde
    arg1.should_receive(:to_s).any_number_of_times.and_return("Hello")
260 23 Shri Borde
    lambda{ @widget.foo(arg1, 123) }.should raise_error(TypeError)
261 23 Shri Borde
262 23 Shri Borde
    arg2 = mock("arg2")
263 23 Shri Borde
    arg2.should_receive(:to_int).any_number_of_times.and_return(123)
264 23 Shri Borde
    lambda{ @widget.foo("Hello", arg2) }.should raise_error(TypeError)
265 21 Shri Borde
  end
266 21 Shri Borde
267 21 Shri Borde
  it "raises TypeError if argument is nil" do
268 21 Shri Borde
    lambda{ @widget.foo(nil) }.should raise_error(TypeError)
269 21 Shri Borde
  end
270 21 Shri Borde
271 21 Shri Borde
# # This is a *non-example*. It should not be included in the specs
272 21 Shri Borde
# it "accepts and ignores a block" do
273 21 Shri Borde
#   # This is a *non-example*. It should not be included in the specs
274 21 Shri Borde
#   @widget.foo { }.should == @widget.foo
275 21 Shri Borde
# end
276 21 Shri Borde
277 21 Shri Borde
# # This is a *non-example*. It should not be included in the specs
278 21 Shri Borde
# it "requires exactly 2 arguments" do
279 21 Shri Borde
#   # This is a *non-example*. It should not be included in the specs
280 21 Shri Borde
#   lambda { @widget.foo("Hello") }.should raise_error(ArgumentError)
281 21 Shri Borde
#   lambda { @widget.foo("Hello", 123, nil) }.should raise_error(ArgumentError)
282 21 Shri Borde
# end
283 21 Shri Borde
284 21 Shri Borde
  #
285 21 Shri Borde
  # For methods which take a block
286 21 Shri Borde
  #
287 21 Shri Borde
  
288 21 Shri Borde
  it "raises LocalJumpError if no block is given" do
289 21 Shri Borde
    lambda{ @widget.foo }.should raise_error(LocalJumpError)
290 21 Shri Borde
  end
291 21 Shri Borde
292 21 Shri Borde
  it "returns the result of the block" do
293 21 Shri Borde
    @widget.foo { :end_of_block }.should == :end_of_block
294 21 Shri Borde
  end
295 21 Shri Borde
296 21 Shri Borde
  it "calls block with a SomeType argument" do
297 21 Shri Borde
    @widget.foo do {|arg1|
298 21 Shri Borde
      arg1.should be_kind_of(SomeType)
299 21 Shri Borde
    end
300 21 Shri Borde
  end
301 21 Shri Borde
302 21 Shri Borde
  it "propagates exception raised inside block" do
303 21 Shri Borde
    lambda { @widget.foo { raise "exception from block" }.should raise_error(RuntimeError)
304 21 Shri Borde
  end
305 21 Shri Borde
306 21 Shri Borde
  it "rescues exception raised inside block" do
307 21 Shri Borde
    @widget.foo { raise "exception from block" }.should == "some result"
308 21 Shri Borde
  end
309 21 Shri Borde
310 21 Shri Borde
  def WidgetSpecs.call_foo_with_block_that_returns
311 21 Shri Borde
    @widget.foo { return :return_from_block }
312 21 Shri Borde
    flunk
313 21 Shri Borde
  end
314 21 Shri Borde
  
315 21 Shri Borde
  it "allows return from block" do
316 21 Shri Borde
    WidgetSpecs.call_foo_with_block_that_returns.should == :return_from_block
317 21 Shri Borde
  end
318 21 Shri Borde
end
319 21 Shri Borde
320 21 Shri Borde
321 16 Shri Borde
322 1
h2. 2. Language
323 1
324 1
For the language specs, there is nothing as convenient or as concrete as a particular method to spec. Review the discussion of the "organization":/wiki/rubyspec/Organization of the language specs. The general conventions apply here: use simple English to describe the behavior of the _language entities_ and only add detail as needed. Use a single @describe@ block initially and add distinguishing @describe@ blocks as necessary. Use @it@ rather than @specify@ whenever possible.