An executable specification for the Ruby programming language
Generally, RSpec specs describe the expected behavior of code. While RSpec is fairly young, there are some conventions for writing specs. The ruby/spec specs 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.
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.
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.)
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.
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.
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. Code raises are rescues the class of an exception, not the error message, which is provided for human consumption. The class of the exception is the interface that the specs are testing.
This is a guiding principle, not a hard and fast rule.
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:
and the following in core/thread/inspect_spec.rb:
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”.
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.
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.
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.
Nested describe
blocks should be used only when absolutely necessary to
make the specs easier to understand. Various automated process scripts depend
on the describe
string having the format explained above. Consequently,
when using nested describe
blocks, ensure that the first block begins with
the method name.
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.
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 documentation.
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:
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.
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.
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.)
In rubyspec/core/array/shared/collect.rb
In rubyspec/core/array/collect_spec.rb
In rubyspec/core/array/map_spec.rb
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.
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).
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
.
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.
The point of ruby/spec 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.
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.
It is conceivable that user code like the following exists:
In such case, the behavior of the following code would be different:
In the second case, the expected behavior is restored if the Silly class is
modified to implement a #respond_to?(:to_int)
.
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.
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?
.
For the language specs, there is nothing as convenient or as concrete as a
particular method to spec. Review the discussion of the
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.