Style is what separates the good from the great.
-- Bozhidar Batsov
This document is a fork of what seems to be a standard ruby and rails style guide in the community, modified for how things are done at TeamPages.com.
This Ruby style guide recommends best practices so that real-world Ruby programmers can write code that can be maintained by other real-world Ruby programmers. A style guide that reflects real-world usage gets used, and a style guide that holds to an ideal that has been rejected by the people it is supposed to help risks not getting used at all โ no matter how good it is.
- Source Code Layout
- Syntax
- Naming
- Comments
- Annotations
- Classes
- Exceptions
- Collections
- Strings
- Regular Expressions
- Percent Literals
- Metaprogramming
- Misc
- Configuration
- Routing
- Controllers
- Models
- Migrations
- Views
- Assets
- Mailers
- Bundler
- Priceless Gems
- Flawed Gems
- Managing processes
- Testing Rails applications
- Cucumber
- RSpec
Nearly everybody is convinced that every style but their own is ugly and unreadable. Leave out the "but their own" and they're probably right...
-- Jerry Coffin (on indentation)
-
Use
UTF-8
as the source file encoding. -
Use two spaces per indentation level.
# good def some_method do_something end # bad - four spaces def some_method do_something end
-
Use Unix-style line endings. (*BSD/Solaris/Linux/OSX users are covered by default, Windows users have to be extra careful.)
-
If you're using Git you might want to add the following configuration setting to protect your project from Windows line endings creeping in:
$ git config --global core.autocrlf true
-
-
Use spaces around operators, after commas, colons and semicolons, around
{
and before}
. Whitespace might be (mostly) irrelevant to the Ruby interpreter, but its proper use is the key to writing easily readable code.sum = 1 + 2 a, b = 1, 2 1 > 2 ? true : false; puts 'Hi' [1, 2, 3].each { |e| puts e }
The only exception is when using the exponent operator:
# bad e = M * c ** 2 # good e = M * c**2
-
Spaces should be used after
(
,[
and before]
,)
.some( arg ).other [ 1, 2, 3 ].length def( blah = nil ) end
-
Indent
when
as deep ascase
.case when song.name == 'Misty' puts 'Not again!' when song.duration > 120 puts 'Too long!' when Time.now.hour > 21 puts "It's too late" else song.play end kind = case year when 1850..1889 then 'Blues' when 1890..1909 then 'Ragtime' when 1910..1929 then 'New Orleans Jazz' when 1930..1939 then 'Swing' when 1940..1950 then 'Bebop' else 'Jazz' end
-
Use empty lines between
def
s and to break up a method into logical paragraphs.def some_method data = initialize(options) data.manipulate! data.result end def some_method result end
-
Add comment lines above all method declarations
# -------------------------------------------------------------- some_method def some_method end # ------------------------------------------------------------- other_method def other_method( a, b, c ) end
-
Align the parameters of a method call if they span over multiple lines.
# starting point (line is too long) def send_mail( source ) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text ) end # bad (normal indent) def send_mail( source ) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text ) end # bad (double indent) def send_mail( source ) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text ) end # good def send_mail( source ) Mailer.deliver( to: '[email protected]', from: '[email protected]', subject: 'Important message', body: source.text ) end
-
Use RDoc and its conventions for API documentation. Don't put an empty line between the comment block and the
def
. -
Keep lines fewer than 80 characters.
-
Avoid trailing whitespace.
-
Use
def
with parentheses when there are arguments. Omit the parentheses when the method doesn't accept any arguments.def some_method # body omitted end def some_method_with_arguments( arg1, arg2 ) # body omitted end
-
Never use
for
, unless you know exactly why. Most of the time iterators should be used instead.for
is implemented in terms ofeach
(so you're adding a level of indirection), but with a twist -for
doesn't introduce a new scope (unlikeeach
) and variables defined in its block will be visible outside it.arr = [ 1, 2, 3 ] # bad for elem in arr do puts elem end # good arr.each { |elem| puts elem }
-
Never use
then
for multi-lineif/unless
.# bad if some_condition then # body omitted end # good if some_condition # body omitted end
-
Favor the ternary operator(
?:
) over shortif/then/else/end
constructs. Do not use single line if bool then ... else ... end statements. Trailing ifs are acceptable for flow coontrol.# bad result = if some_condition then something else something_else end # good result = some_condition ? something : something_else # good next if condition_true
-
Use one expression per branch in a ternary operator. This also means that ternary operators must not be nested. Prefer
if/else
constructs in these cases.# bad some_condition ? (nested_condition ? nested_something : nested_something_else) : something_else # good if some_condition nested_condition ? nested_something : nested_something_else else something_else end
-
Never use
if x: ...
- it is removed in Ruby 1.9. Use the ternary operator instead.# bad result = if some_condition: something else something_else end # good result = some_condition ? something : something_else
-
Never use
if x; ...
. Use the ternary operator instead. -
Use
when x then ...
for one-line cases. The alternative syntaxwhen x: ...
is removed in Ruby 1.9. -
Never use
when x; ...
. See the previous rule. -
Use
&&/||
for boolean expressions,and/or
for control flow. (Rule of thumb: If you have to use outer parentheses, you are using the wrong operators.)# boolean expression if some_condition && some_other_condition do_something end # control flow document.saved? or document.save!
-
Avoid multi-line
?:
(the ternary operator), useif/unless
instead. -
Favor modifier
if/unless
usage when you have a single-line body. Another good alternative is the usage of control flowand/or
.# bad if some_condition do_something end # good do_something if some_condition # another good option some_condition and do_something
-
Favor
if
overunless
at all times.# bad do_something unless some_condition # good do_something if !some_condition # another good option some_condition or do_something
-
Never use
unless
withelse
. Rewrite these with the positive case first.# bad unless success? puts 'failure' else puts 'success' end # good if success? puts 'success' else puts 'failure' end
-
Don't use parentheses around the condition of an
if/unless/while
, unless the condition contains an assignment (see "Using the return value of=
" below).# bad if ( x > 10 ) # body omitted end # good if x > 10 # body omitted end # ok if ( x = self.next_value ) # body omitted end
-
Favor modifier
while/until
usage when you have a single-line body.# bad while some_condition do_something end # good do_something while some_condition
-
Favor
until
overwhile
for negative conditions.# bad do_something while !some_condition # good do_something until some_condition
-
Omit parentheses around parameters for methods that are part of an internal DSL (e.g. Rake, Rails, RSpec), methods that are with "keyword" status in Ruby (e.g.
attr_reader
,puts
) and attribute access methods. Use parentheses around the arguments of all other method invocations.class Person attr_reader :name, :age # omitted end temperance = Person.new( 'Temperance', 30 ) temperance.name puts temperance.age x = Math.sin( y ) array.delete( e )
-
Prefer
{...}
overdo...end
for single-line blocks. Avoid using{...}
for multi-line blocks (multiline chaining is always ugly). Always usedo...end
for "control flow" and "method definitions" (e.g. in Rakefiles and certain DSLs). Avoiddo...end
when chaining.names = ['Bozhidar', 'Steve', 'Sarah'] # good names.each { |name| puts name } # bad names.each do |name| puts name end # good names.select { |name| name.start_with?('S') }.map { |name| name.upcase } # bad names.select do |name| name.start_with?('S') end.map { |name| name.upcase }
Some will argue that multiline chaining would look OK with the use of {...}, but they should ask themselves - it this code really readable and can't the blocks contents be extracted into nifty methods.
-
Avoid
return
where not required.# bad def some_method(some_arr) return some_arr.size end # good def some_method(some_arr) some_arr.size end
-
Avoid
self
where not required.# bad def ready? if self.last_reviewed_at > self.last_updated_at self.worker.update(self.content, self.options) self.status = :in_progress end self.status == :verified end # good def ready? if last_reviewed_at > last_updated_at worker.update(content, options) self.status = :in_progress end status == :verified end
-
As a corollary, avoid shadowing methods with local variables unless they are both equivalent
class Foo attr_accessor :options # ok def initialize(options) self.options = options # both options and self.options are equivalent here end # bad def do_something(options = {}) unless options[:when] == :later output(self.options[:message]) end end # good def do_something(params = {}) unless params[:when] == :later output(options[:message]) end end end
-
Use spaces around the
=
operator when assigning default values to method parameters:# bad def some_method(arg1=:default, arg2=nil, arg3=[]) # do something... end # good def some_method(arg1 = :default, arg2 = nil, arg3 = []) # do something... end
While several Ruby books suggest the first style, the second is much more prominent in practice (and arguably a bit more readable).
-
Avoid line continuation (\) where not required. In practice, avoid using line continuations at all.
# bad result = 1 - \ 2 # good (but still ugly as hell) result = 1 \ - 2
-
Using the return value of
=
(an assignment) is ok, but surround the assignment with parenthesis.# good - shows intended use of assignment if ( v = array.grep( /foo/ ) ) ... # bad if v = array.grep(/foo/) ... # also good - shows intended use of assignment and has correct precedence. if (v = self.next_value) == 'hello' ...
-
Use
||=
freely to initialize variables.# set name to Bozhidar, only if it's nil or false name ||= 'Bozhidar'
-
Don't use
||=
to initialize boolean variables. (Consider what would happen if the current value happened to befalse
.)# bad - would set enabled to true even if it was false enabled ||= true # good enabled = true if enabled.nil?
-
Avoid using Perl-style special variables (like
$0-9
, `$``, etc. ). They are quite cryptic and their use in anything but one-liner scripts is discouraged. -
Never put a space between a method name and the opening parenthesis.
# bad f (3 + 2) + 1 # good f( 3 + 2 ) + 1
-
If the first argument to a method begins with an open parenthesis, always use parentheses in the method invocation. For example, write
f((3 + 2) + 1)
. -
Always run the Ruby interpreter with the
-w
option so it will warn you if you forget either of the rules above! -
When the keys of your hash are symbols use the Ruby 1.9 hash literal syntax.
# bad hash = { :one => 1, :two => 2 } # good hash = { one: 1, two: 2 }
-
Use the new lambda literal syntax.
# bad lambda = lambda { |a, b| a + b } lambda.call(1, 2) # good lambda = ->(a, b) { a + b } lambda.(1, 2)
-
Use
_
for unused block parameters.# bad result = hash.map { |k, v| v + 1 } # good result = hash.map { |_, v| v + 1 }
The only real difficulties in programming are cache invalidation and naming things.
-- Phil Karlton
-
Use
snake_case
for methods and variables. -
Use
CamelCase
for classes and modules. (Keep acronyms like HTTP, RFC, XML uppercase.) -
Use
SCREAMING_SNAKE_CASE
for other constants. -
The names of predicate methods (methods that return a boolean value) should end in a question mark. (i.e.
Array#empty?
). -
The names of potentially "dangerous" methods (i.e. methods that modify
self
or the arguments,exit!
(doesn't run the finalizers likeexit
does), etc.) should end with an exclamation mark if there exists a safe version of that dangerous method.# bad - there is not matching 'safe' method class Person def update! end end # good class Person def update end end # good class Person def update! end def update end end
-
Define the non-bang (safe) method in terms of the bang (dangerous) one if possible.
class Array def flatten_once! res = [] each do |e| [*e].each { |f| res << f } end replace(res) end def flatten_once dup.flatten_once! end end
-
When using
reduce
with short blocks, name the arguments|a, e|
(accumulator, element). -
When defining binary operators, name the argument
other
.def +(other) # body omitted end
-
Prefer
map
overcollect
,find
overdetect
,select
overfind_all
,reduce
overinject
andsize
overlength
. This is not a hard requirement; if the use of the alias enhances readability, it's ok to use it. The rhyming methods are inherited from Smalltalk and are not common in other programming languages. The reason the use ofselect
is encouraged overfind_all
is that it goes together nicely withreject
and its name is pretty self-explanatory.
Good code is its own best documentation. As you're about to add a comment, ask yourself, "How can I improve the code so that this comment isn't needed?" Improve the code and then document it to make it even clearer.
-- Steve McConnell
-
Write self-documenting code and ignore the rest of this section. Seriously!
-
Comments longer than a word are capitalized and use punctuation. Use one space after periods.
-
Avoid superfluous comments.
# bad counter += 1 # increments counter by one
-
Keep existing comments up-to-date. An outdated is worse than no comment at all.
Good code is like a good joke - it needs no explanation.
-- Russ Olsen
- Avoid writing comments to explain bad code. Refactor the code to make it self-explanatory. (Do or do not - there is no try. --Yoda)
-
Annotations should usually be written on the line immediately above the relevant code.
-
The annotation keyword is followed by a colon and a space, then a note describing the problem.
-
If multiple lines are required to describe the problem, subsequent lines should be indented two spaces after the
#
.def bar # FIXME: This has crashed occasionally since v3.2.1. It may # be related to the BarBazUtil upgrade. baz(:quux) end
-
In cases where the problem is so obvious that any documentation would be redundant, annotations may be left at the end of the offending line with no note. This usage should be the exception and not the rule.
def bar sleep 100 # OPTIMIZE end
-
Use
TODO
to note missing features or functionality that should be added at a later date. -
Use
FIXME
to note broken code that needs to be fixed. -
Use
OPTIMIZE
to note slow or inefficient code that may cause performance problems. -
Use
HACK
to note code smells where questionable coding practices were used and should be refactored away. -
Use
REVIEW
to note anything that should be looked at to confirm it is working as intended. For example:REVIEW: Are we sure this is how the client does X currently?
-
Use other custom annotation keywords if it feels appropriate, but be sure to document them in your project's
README
or similar.
-
When designing class hierarchies make sure that they conform to the Liskov Substitution Principle.
-
Try to make your classes as [SOLID](http://en.wikipedia.org/wiki/SOLID_(object-oriented_design\)) as possible.
-
Always supply a proper
to_s
method for classes that represent domain objects.class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def to_s "#@first_name #@last_name" end end
-
Use the
attr
family of functions to define trivial accessors or mutators.# bad class Person def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end def first_name @first_name end def last_name @last_name end end # good class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end
-
Consider using
Struct.new
, which defines the trivial accessors, constructor and comparison operators for you.# good class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # better class Person < Struct.new (:first_name, :last_name) end
-
Consider adding factory methods to provide additional sensible ways to create instances of a particular class.
class Person def self.create(options_hash) # body omitted end end
-
Prefer duck-typing over inheritance.
# bad class Animal # abstract method def speak end end # extend superclass class Duck < Animal def speak puts 'Quack! Quack' end end # extend superclass class Dog < Animal def speak puts 'Bau! Bau!' end end # good class Duck def speak puts 'Quack! Quack' end end class Dog def speak puts 'Bau! Bau!' end end
-
Avoid the usage of class (
@@
) variables due to their "nasty" behavior in inheritance.class Parent @@class_var = 'parent' def self.print_class_var puts @@class_var end end class Child < Parent @@class_var = 'child' end Parent.print_class_var # => will print "child"
As you can see all the classes in a class hierarchy actually share one class variable. Class instance variables should usually be preferred over class variables.
-
Assign proper visibility levels to methods (
private
,protected
) in accordance with their intended usage. Don't go off leaving everythingpublic
(which is the default). After all we're coding in Ruby now, not in Python. -
Indent the
public
,protected
, andprivate
methods as much the method definitions they apply to. Leave one blank line above them.class SomeClass def public_method # ... end private def private_method # ... end end
-
Use
def self.method
to define singleton methods. This makes the methods more resistant to refactoring changes.class TestClass # bad def TestClass.some_method # body omitted end # good def self.some_other_method # body omitted end # Also possible and convenient when you # have to define many singleton methods. class << self def first_method # body omitted end def second_method_etc # body omitted end end end
-
Signal exceptions using the
fail
keyword. Useraise
only when catching an exception and re-raising it (because here you're not failing, but explicitly and purposefully raising an exception).begin fail 'Oops'; rescue => error raise if error.message != 'Oops' end
-
Never return form an
ensure
block. If you explicitly return from a method inside anensure
block, the return will take precedence over any exception being raised, and the method will return as if no exception had been raised at all. In effect, the exception will be silently thrown away.def foo begin fail ensure return 'very bad idea' end end
-
Use implicit begin blocks when possible.
# bad def foo begin # main logic goes here rescue # failure handling goes here end end # good def foo # main logic goes here rescue # failure handling goes here end
-
Mitigate the proliferation of
begin
blocks via the use of contingency methods (a term coined by Avdi Grimm).# bad begin something_that_might_fail rescue IOError # handle IOError end begin something_else_that_might_fail rescue IOError # handle IOError end # good def with_io_error_handling yield rescue # handle IOError end with_io_error_handling { something_that_might_fail } with_io_error_handling { something_else_that_might_fail }
-
Don't suppress exceptions.
# bad begin # an exception occurs here rescue SomeError # the rescue clause does absolutely nothing end # bad do_something rescue nil
-
Don't use exceptions for flow of control.
# bad begin n / d rescue ZeroDivisionError puts 'Cannot divide by 0!' end # good if d.zero? puts 'Cannot divide by 0!' else n / d end
-
Avoid rescuing the
Exception
class. This will trap signals and calls toexit
, requiring you tokill -9
the process.# bad begin # calls to exit and kill signals will be caught (except kill -9) exit rescue Exception puts "you didn't really want to exit, right?" # exception handling end # good begin # a blind rescue rescues from StandardError, not Exception as many # programmers assume. rescue => e # exception handling end # also good begin # an exception occurs here rescue StandardError => e # exception handling end
-
Put more specific exceptions higher up the rescue chain, otherwise they'll never be rescued from.
# bad begin # some code rescue Exception => e # some handling rescue StandardError => e # some handling end # good begin # some code rescue StandardError => e # some handling rescue Exception => e # some handling end
-
Release external resources obtained by your program in an ensure block.
f = File.open('testfile') begin # .. process rescue # .. handle error ensure f.close unless f.nil? end
-
Favor the use of exceptions for the standard library over introducing new exception classes.
-
Prefer literal array and hash creation notation (unless you need to pass parameters to their constructors, that is).
# bad arr = Array.new hash = Hash.new # good arr = [] hash = {}
-
Prefer
%w
to the literal array syntax when you need an array of strings.# bad STATES = ['draft', 'open', 'closed'] # good STATES = %w(draft open closed)
-
Avoid the creation of huge gaps in arrays.
arr = [] arr[100] = 1 # now you have an array with lots of nils
-
Use
Set
instead ofArray
when dealing with unique elements.Set
implements a collection of unordered values with no duplicates. This is a hybrid ofArray
's intuitive inter-operation facilities andHash
's fast lookup. -
Use symbols instead of strings as hash keys.
# bad hash = { 'one' => 1, 'two' => 2, 'three' => 3 } # good hash = { one: 1, two: 2, three: 3 }
-
Avoid the use of mutable object as hash keys.
-
Use the new 1.9 literal hash syntax in preference to the hashrocket syntax.
# bad hash = { :one => 1, :two => 2, :three => 3 } # good hash = { one: 1, two: 2, three: 3 }
-
Rely on the fact that hashes in 1.9 are ordered.
-
Never modify a collection while traversing it.
-
Prefer string interpolation instead of string concatenation:
# bad email_with_name = user.name + ' <' + user.email + '>' # good email_with_name = "#{user.name} <#{user.email}>"
-
Consider padding string interpolation code with space. It more clearly sets the code apart from the string.
"#{ user.last_name }, #{ user.first_name }"
-
Prefer single-quoted strings when you don't need string interpolation or special symbols such as
\t
,\n
,'
, etc.# bad name = "Bozhidar" # good name = 'Bozhidar'
-
Don't use
{}
around instance variables being interpolated into a string.class Person attr_reader :first_name, :last_name def initialize( first_name, last_name ) @first_name = first_name @last_name = last_name end # bad def to_s "#{@first_name} #{@last_name}" end # good def to_s "#@first_name #@last_name" end end
-
Avoid using
String#+
when you need to construct large data chunks. Instead, useString#<<
. Concatenation mutates the string instance in-place and is always faster thanString#+
, which creates a bunch of new string objects.# good and also fast html = '' html << '<h1>Page title</h1>' paragraphs.each do |paragraph| html << "<p>#{paragraph}</p>" end
-
Don't use regular expressions if you just need plain text search in string:
string['text']
-
For simple constructions you can use regexp directly through string index.
match = string[/regexp/] # get content of matched regexp first_group = string[/text(grp)/, 1] # get content of captured group string[/text (grp)/, 1] = 'replace' # string => 'text replace'
-
Use non capturing groups when you don't use captured result of parenthesis.
/(first|second)/ # bad /(?:first|second)/ # good
-
Avoid using $1-9 as it can be hard to track what they contain. Named groups can be used instead.
# bad /(regexp)/ =~ string ... process $1 # good /(?<meaningful_var>regexp)/ =~ string ... process meaningful_var
-
Character classes have only few special characters you should care about:
^
,-
,\
,]
, so don't escape.
or brackets in[]
. -
Be careful with
^
and$
as they match start/end of line, not string endings. If you want to match the whole string use:\A
and\z
(not to be confused with\Z
which is the equivalent of/\n?\z/
).string = "some injection\nusername" string[/^username$/] # matches string[/\Ausername\z/] # don't match
-
Use
x
modifier for complex regexps. This makes them more readable and you can add some useful comments. Just be careful as spaces are ignored.regexp = %r{ start # some text \s # white space char (group) # first group (?:alt1|alt2) # some alternation end }x
-
For complex replacements
sub
/gsub
can be used with block or hash.
-
Use
%w
freely.STATES = %w(draft open closed)
-
Use
%()
for single-line strings which require both interpolation and embedded double-quotes. For multi-line strings, prefer heredocs.# bad (no interpolation needed) %(<div class="text">Some text</div>) # should be '<div class="text">Some text</div>' # bad (no double-quotes) %(This is #{quality} style) # should be "This is #{quality} style" # bad (multiple lines) %(<div>\n<span class="big">#{exclamation}</span>\n</div>) # should be a heredoc. # good (requires interpolation, has quotes, single line) %(<tr><td class="name">#{name}</td>)
-
Use
%r
only for regular expressions matching more than one '/' character.# bad %r(\s+) # still bad %r(^/(.*)$) # should be /^\/(.*)$/ # good %r(^/blog/2011/(.*)$)
-
Avoid
%q
,%Q
,%x
,%s
, and%W
. -
Prefer
()
as delimiters for all%
literals.
-
Do not mess around in core classes when writing libraries. (Do not monkey patch them.)
-
The block form of
class_eval
is preferable to the string-interpolated form.-
when you use the string-interpolated form, always supply
__FILE__
and__LINE__
, so that your backtraces make sense:class_eval 'def use_relative_model_naming?; true; end', __FILE__, __LINE__
-
define_method
is preferable toclass_eval{ def ... }
-
-
When using
class_eval
(or othereval
) with string interpolation, add a comment block showing its appearance if interpolated (a practice I learned from the rails code):# from activesupport/lib/active_support/core_ext/string/output_safety.rb UNSAFE_STRING_METHODS.each do |unsafe_method| if 'String'.respond_to?(unsafe_method) class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{unsafe_method}(*args, &block) # def capitalize(*args, &block) to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block) end # end def #{unsafe_method}!(*args) # def capitalize!(*args) @dirty = true # @dirty = true super # super end # end EOT end end
-
avoid using
method_missing
for metaprogramming. Backtraces become messy; the behavior is not listed in#methods
; misspelled method calls might silently work (nukes.launch_state = false
). Consider using delegation, proxy, ordefine_method
instead. If you must, usemethod_missing
,-
be sure to also define
respond_to_missing?
-
only catch methods with a well-defined prefix, such as
find_by_*
-- make your code as assertive as possible. -
call
super
at the end of your statement -
delegate to assertive, non-magical methods:
# bad def method_missing?(meth, *args, &block) if /^find_by_(?<prop>.*)/ =~ meth # ... lots of code to do a find_by else super end end # good def method_missing?(meth, *args, &block) if /^find_by_(?<prop>.*)/ =~ meth find_by(prop, *args, &block) else super end end # best of all, though, would to define_method as each findable attribute is declared
-
-
Write
ruby -w
safe code. -
Avoid hashes as optional parameters. Does the method do too much?
-
Avoid methods longer than 10 LOC (lines of code). Ideally, most methods will be shorter than 5 LOC. Empty lines do not contribute to the relevant LOC.
-
Avoid parameter lists longer than three or four parameters.
-
If you really have to, add "global" methods to Kernel and make them private.
-
Use class instance variables instead of global variables.
#bad $foo_bar = 1 #good class Foo class << self attr_accessor :bar end end Foo.bar = 1
-
Avoid
alias
whenalias_method
will do. -
Use
OptionParser
for parsing complex command line options andruby -s
for trivial command line options. -
Code in a functional way, avoiding mutation when that makes sense.
-
Avoid needless metaprogramming.
-
Do not mutate arguments unless that is the purpose of the method.
-
Avoid more than three levels of block nesting.
-
Be consistent. In an ideal world, be consistent with these guidelines.
-
Use common sense.
-
Put custom initialization code in
config/initializers
. The code in initializers executes on application startup. -
The initialization code for each gem should be in a separate file with the same name as the gem, for example
carrierwave.rb
,active_admin.rb
, etc. -
Adjust accordingly the settings for development, test and production environment (in the corresponding files under
config/environments/
)-
Mark additional assets for precompilation (if any):
```Ruby # config/environments/production.rb # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js ) ```
-
-
Create an additional
staging
environment that closely resembles theproduction
one.
-
When you need to add more actions to a RESTful resource (do you really need them at all?) use
member
andcollection
routes.# bad get 'subscriptions/:id/unsubscribe' resources :subscriptions # good resources :subscriptions, :member => { :unsubscribe => :get } # bad get 'photos/search' resources :photos # good resources :photos,:collection => { :search => :get }
-
Use nested routes to express better the relationship between ActiveRecord models.
class Post < ActiveRecord::Base has_many :comments end class Comments < ActiveRecord::Base belongs_to :post end # routes.rb resources :posts do resources :comments end
-
Use namespaced routes to group related actions.
namespace :admin do # Directs /admin/products/* to Admin::ProductsController # (app/controllers/admin/products_controller.rb) resources :products end
-
Never use the legacy wild controller route. This route will make all actions in every controller accessible via GET requests.
# very bad match ':controller(/:action(/:id(.:format)))'
- Keep the controllers skinny - they should only retrieve data for the view layer and shouldn't contain any business logic (all the business logic should naturally reside in the model).
- Each controller action should (ideally) invoke only one method other than an initial find or new.
- Share no more than two instance variables between a controller and a view.
-
Introduce non-ActiveRecord model classes freely.
-
Name the models with meaningful (but short) names without abbreviations.
-
If you need model objects that support ActiveRecord behavior like validation use the ActiveAttr gem.
class Message include ActiveAttr::Model attribute :name attribute :email attribute :content attribute :priority attr_accessible :name, :email, :content validates_presence_of :name validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i validates_length_of :content, :maximum => 500 end
For a more complete example refer to the RailsCast on the subject.
-
Avoid altering ActiveRecord defaults (table names, primary key, etc) unless you have a very good reason (like a database that's not under your control).
-
Group macro-style methods (
has_many
,validates
, etc) in the beginning of the class definition. -
Prefer
has_many :through
tohas_and_belongs_to_many
. Usinghas_many :through
allows additional attributes and validations on the join model.# using has_and_belongs_to_many class User < ActiveRecord::Base has_and_belongs_to_many :groups end class Group < ActiveRecord::Base has_and_belongs_to_many :users end # prefered way - using has_many :through class User < ActiveRecord::Base has_many :memberships has_many :groups, through: :memberships end class Membership < ActiveRecord::Base belongs_to :user belongs_to :group end class Group < ActiveRecord::Base has_many :memberships has_many :users, through: :memberships end
-
When a custom validation is used more than once or the validation is some regular expression mapping, create a custom validator file.
# bad class Person validates :email, format: { with: /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i } end # good class EmailValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i end end class Person validates :email, email: true end
-
All custom validators should be moved to a shared gem.
-
Use named scopes freely.
-
When a named scope, defined with a lambda and parameters, becomes too complicated it is preferable to make a class method instead which serves the same purpose of the named scope and returns and
ActiveRecord::Relation
object. -
Beware of the behavior of the
update_attribute
method. It doesn't run the model validations (unlikeupdate_attributes
) and could easily corrupt the model state. -
Use user-friendly URLs. Show some descriptive attribute of the model in the URL rather than its
id
. There is more than one way to achieve this:-
Override the
to_param
method of the model. This method is used by Rails for constructing an URL to the object. The default implementation returns theid
of the record as a String. It could be overridden to include another human-readable attribute.```Ruby class Person def to_param "#{id} #{name}".parameterize end end ``` In order to convert this to a URL-friendly value, `parameterize` should be called on the string. The `id` of the object needs to be at the beginning so that it could be found by the `find` method of ActiveRecord.
-
Use the
friendly_id
gem. It allows creation of human-readable URLs by using some descriptive attribute of the model instead of itsid
.```Ruby class Person extend FriendlyId friendly_id :name, use: :slugged end ``` Check the [gem documentation](https://github.com/norman/friendly_id) for more information about its usage.
-
-
Keep the
schema.rb
under version control. -
Use
rake db:schema:load
instead ofrake db:migrate
to initialize an empty database. -
Use
rake db:test:prepare
to update the schema of the test database. -
Avoid setting defaults in the tables themselves. Use the model layer instead.
def amount self[:amount] || 0 end
While the use of
self[:attr_name]
is considered fairly idiomatic, you might also consider using the slightly more verbose (and arguably more readable)read_attribute
instead:def amount read_attribute( :amount ) || 0 end
-
When writing constructive migrations (adding tables or columns), ensure that the down mehtod is written
-
When necessary, use execute("RAW SQL") to achieve missing functionality
- Never call the model layer directly from a view.
- Never make complex formatting in the views, export the formatting to a method in the view helper or the model.
- Mitigate code duplication by using partial templates and layouts.
- Add
client side validation
for the custom validators. The steps to do this are:
-
Declare a custom validator which extends
ClientSideValidations::Middleware::Base
```Ruby module ClientSideValidations::Middleware class Email < Base def response if request.params[:email] =~ /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/i self.status = 200 else self.status = 404 end super end end end ```
-
Create a new file
public/javascripts/rails.validations.custom.js.coffee
and add a reference to it in yourapplication.js.coffee
file:```Ruby # app/assets/javascripts/application.js.coffee #= require rails.validations.custom ```
-
Add your client-side validator:
```Ruby #public/javascripts/rails.validations.custom.js.coffee clientSideValidations.validators.remote['email'] = (element, options) -> if $.ajax({ url: '/validators/email.json', data: { email: element.val() }, async: false }).status == 404 return options.message || 'invalid e-mail format' ```
-
-
No strings or other locale specific settings should be used in the views, models and controllers. These texts should be moved to the locale files in the
config/locales
directory. -
When the labels of an ActiveRecord model need to be translated, use the
activerecord
scope:en: activerecord: models: user: Member attributes: user: name: "Full name"
Then
User.model_name.human
will return "Member" andUser.human_attribute_name("name")
will return "Full name". These translations of the attributes will be used as labels in the views. -
Separate the texts used in the views from translations of ActiveRecord attributes. Place the locale files for the models in a folder
models
and the texts used in the views in folderviews
.-
When organization of the locale files is done with additional directories, these directories must be described in the
application.rb
file in order to be loaded.```Ruby # config/application.rb config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')] ```
-
-
Place the shared localization options, such as date or currency formats, in files under the root of the
locales
directory. -
Use the short form of the I18n methods:
I18n.t
instead ofI18n.translate
andI18n.l
instead ofI18n.localize
. -
Use "lazy" lookup for the texts used in views. Let's say we have the following structure:
en: users: show: title: "User details page"
The value for
users.show.title
can be looked up in the templateapp/views/users/show.html.haml
like this:= t '.title'
-
Use the dot-separated keys in the controllers and models instead of specifying the
:scope
option. The dot-separated call is easier to read and trace the hierarchy.# use this call I18n.t 'activerecord.errors.messages.record_invalid' # instead of this I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
-
More detailed information about the Rails i18n can be found in the [Rails Guides] (http://guides.rubyonrails.org/i18n.html)
-
Name the mailers
SomethingMailer
. Without the Mailer suffix it isn't immediately apparent what's a mailer and which views are related to the mailer. -
Provide both HTML and plain-text view templates.
-
Enable errors raised on failed mail delivery in your development environment. The errors are disabled by default.
# config/environments/development.rb config.action_mailer.raise_delivery_errors = true
-
Use
smtp.gmail.com
for SMTP server in the development environment (unless you have local SMTP server, of course).# config/environments/development.rb config.action_mailer.smtp_settings = { address: 'smtp.gmail.com', # more settings }
-
Provide default settings for the host name.
# config/environments/development.rb config.action_mailer.default_url_options = {host: "#{local_ip}:3000"} # config/environments/production.rb config.action_mailer.default_url_options = {host: 'your_site.com'} # in your mailer class default_url_options[:host] = 'your_site.com'
-
If you need to use a link to your site in an email, always use the
_url
, not_path
methods. The_url
methods include the host name and the_path
methods don't.# wrong You can always find more info about this course = link_to 'here', url_for(course_path(@course)) # right You can always find more info about this course = link_to 'here', url_for(course_url(@course))
-
Format the from and to addresses properly. Use the following format:
# in your mailer class default from: 'Your Name <info@your_site.com>'
-
Make sure that the e-mail delivery method for your test environment is set to
test
:# config/environments/test.rb config.action_mailer.delivery_method = :test
-
The delivery method for development and production should be
smtp
:# config/environments/development.rb, config/environments/production.rb config.action_mailer.delivery_method = :smtp
-
When sending html emails all styles should be inline, as some mail clients have problems with external styles. This however makes them harder to maintain and leads to code duplication. There are two similar gems that transform the styles and put them in the corresponding html tags: premailer-rails3 and roadie.
-
Sending emails while generating page response should be avoided. It causes delays in loading of the page and request can timeout if multiple email are send. To overcome this emails can be send in background process with the help of delayed_job gem.
-
Put gems used only for development or testing in the appropriate group in the Gemfile.
-
Use only established gems in your projects. If you're contemplating on including some little-known gem you should do a careful review of its source code first.
-
OS-specific gems will by default result in a constantly changing
Gemfile.lock
for projects with multiple developers using different operating systems. Add all OS X specific gems to adarwin
group in the Gemfile, and all Linux specific gems to alinux
group:# Gemfile group :darwin do gem 'rb-fsevent' gem 'growl' end group :linux do gem 'rb-inotify' end
To require the appropriate gems in the right environment, add the following to
config/application.rb
:platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym Bundler.require(platform)
-
Do not remove the
Gemfile.lock
from version control. This is not some randomly generated file - it makes sure that all of your team members get the same gem versions when they do abundle install
.
One of the most important programming principles is "Don't reinvent the wheel!". If you're faced with a certain task you should always look around a bit for existing solutions, before unrolling your own. Here's a list of some "priceless" gems :
- active_admin - With ActiveAdmin the creation of admin interface for your Rails app is child's play. You get a nice dashboard, CRUD UI and lots more. Very flexible and customizable.
- capybara - Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb. Capybara simulates how a real user would interact with a web application. It is agnostic about the driver running your tests and currently comes with Rack::Test and Selenium support built in. HtmlUnit, WebKit and env.js are supported through external gems. Works great in combination with RSpec & Cucumber.
- carrierwave - the ultimate file upload solution for Rails. Support both local and cloud storage for the uploaded files (and many other cool things). Integrates great with ImageMagick for image post-processing.
- client_side_validations - Fantastic gem that automatically creates JavaScript client-side validations from your existing server-side model validations. Highly recommended!
- compass-rails - Great gem that adds support for some css frameworks. Includes collection of sass mixins that reduces code of css files and help fight with browser incompatibilities.
- cucumber-rails - Cucumber is the premium tool to develop feature tests in Ruby. cucumber-rails provides Rails integration for Cucumber.
- devise - Devise is full-featured authentication solution for Rails applications. In most cases it's preferable to use devise to unrolling your custom authentication solution.
- fabrication - a great fixture replacement (editor's choice).
- factory_girl - an alternative to fabrication. Nice and mature fixture replacement. Spiritual ancestor of fabrication.
- faker - handy gem to generate dummy data (names, addresses, etc).
- feedzirra - Very fast and flexible RSS/Atom feed parser.
- friendly_id - Allows creation of human-readable URLs by using some descriptive attribute of the model instead of its id.
- guard - fantastic gem that monitors file changes and invokes tasks based on them. Loaded with lots of useful extension. Far superior to autotest and watchr.
- haml-rails - haml-rails provides Rails integration for Haml.
- haml - HAML is a concise templating language, considered by many (including yours truly) to be far superior to Erb.
- kaminari - Great paginating solution.
- machinist - Fixtures aren't fun. Machinist is.
- rspec-rails - RSpec is a replacement for Test::MiniTest. I cannot recommend highly enough RSpec. rspec-rails provides Rails integration for RSpec.
- simple_form - once you've used simple_form (or formtastic) you'll never want to hear about Rails's default forms. It has a great DSL for building forms and no opinion on markup.
- simplecov-rcov - RCov formatter for SimpleCov. Useful if you're trying to use SimpleCov with the Hudson contininous integration server.
- simplecov - code coverage tool. Unlike RCov it's fully compatible with Ruby 1.9. Generates great reports. Must have!
- slim - Slim is a concise templating language, considered by many far superior to HAML (not to mention Erb). The only thing stopping me from using Slim massively is the lack of good support in major editors/IDEs. Its performance is phenomenal.
- spork - A DRb server for testing frameworks (RSpec / Cucumber currently) that forks before each run to ensure a clean testing state. Simply put it preloads a lot of test environment and as consequence the startup time of your tests in greatly decreased. Absolute must have!
- sunspot - SOLR powered full-text search engine.
This list is not exhaustive and other gems might be added to it along the road. All of the gems on the list are field tested, have active development and community and are known to be of good code quality.
This is a list of gems that are either problematic or superseded by other gems. You should avoid using them in your projects.
- rmagick - this gem is notorious for its memory consumption. Use minimagick instead.
- autotest - old solution for running tests automatically. Far inferior to guard and watchr.
- rcov - code coverage tool, not compatible with Ruby 1.9. Use SimpleCov instead.
- therubyracer - the use of this gem in production is strongly discouraged as it uses a very large amount of memory. I'd suggest using Mustang instead.
This list is also a work in progress. Please, let me know if you know other popular, but flawed gems.
- If your projects depends on various external processes use foreman to manage them.
The best approach to implementing new features is probably the BDD approach. You start out by writing some high level feature tests (generally written using Cucumber), then you use these tests to drive out the implementation of the feature. First you write view specs for the feature and use those specs to create the relevant views. Afterwards you create the specs for the controller(s) that will be feeding data to the views and use those specs to implement the controller. Finally you implement the models specs and the models themselves.
-
Tag your pending scenarios with
@wip
(work in progress). These scenarios will not be taken into account and will not be marked as failing. When finishing the work on a pending scenario and implementing the functionality it tests, the tag@wip
should be removed in order to include this scenario in the test suite. -
Setup your default profile to exclude the scenarios tagged with
@javascript
. They are testing using the browser and disabling them is recommended to increase the regular scenarios execution speed. -
Setup a separate profile for the scenarios marked with
@javascript
tag.-
The profiles can be configured in the
cucumber.yml
file.```Ruby # definition of a profile: profile_name: --tags @tag_name ```
-
A profile is run with the command:
``` cucumber -p profile_name ```
-
-
If using fabrication for fixtures replacement, use the predefined fabrication steps
-
Do not use the old
web_steps.rb
step definitions! The web steps were removed from the latest version of Cucumber. Their usage leads to the creation of verbose scenarios that do not properly reflect the application domain. -
When checking for the presence of an element with visible text (link, button, etc.) check for the text, not the element id. This can detect problems with the i18n.
-
Create separate features for different functionality regarding the same kind of objects:
# bad Feature: Articles # ... feature implementation ... # good Feature: Article Editing # ... feature implementation ... Feature: Article Publishing # ... feature implementation ... Feature: Article Search # ... feature implementation ...
-
Each feature has three main components
- Title
- Narrative - a short explanation what the feature is about.
- Acceptance criteria - the set of scenarios each made up of individual steps.
-
The most common format is known as the Connextra format.
In order to [benefit] ... A [stakeholder]... Wants to [feature] ...
This format is the most common but is not required, the narrative can be free text depending on the complexity of the feature.
-
Use Scenario Outlines freely to keep the scenarios DRY.
Scenario Outline: User cannot register with invalid e-mail When I try to register with an email "<email>" Then I should see the error message "<error>" Examples: |email |error | | |The e-mail is required| |invalid email |is not a valid e-mail |
-
The steps for the scenarios are in
.rb
files under thestep_definitions
directory. The naming convention for the steps file is[description]_steps.rb
. The steps can be separated into different files based on different criterias. It is possible to have one steps file for each feature (home_page_steps.rb
). There also can be one steps file for all features for a particular object (articles_steps.rb
). -
Use multiline step arguments to avoid repetition
Scenario: User profile Given I am logged in as a user "John Doe" with an e-mail "[email protected]" When I go to my profile Then I should see the following information: |First name|John | |Last name |Doe | |E-mail |user@test.com| # the step: Then /^I should see the following information:$/ do |table| table.raw.each do |field, value| find_field(field).value.should =~ /#{value}/ end end
-
Use compound steps to keep the scenario DRY
# ... When I subscribe for news from the category "Technical News" # ... # the step: When /^I subscribe for news from the category "([^"]*)"$/ do |category| steps %Q{ When I go to the news categories page And I select the category #{category} And I click the button "Subscribe for this category" And I confirm the subscription } end
-
Always use the Capybara negative matchers instead of should_not with positive, they will retry the match for given timeout allowing you to test ajax actions. See Capybara's README for more explanation
-
Use just one expectation per example.
# bad describe ArticlesController do #... describe 'GET new' do it 'assigns new article and renders the new article template' do get :new assigns[:article].should be_a_new Article response.should render_template :new end end # ... end # good describe ArticlesController do #... describe 'GET new' do it 'assigns a new article' do get :new assigns[:article].should be_a_new Article end it 'renders the new article template' do get :new response.should render_template :new end end end
-
Make heavy use of
describe
andcontext
-
Name the
describe
blocks as follows:-
use "description" for non-methods
-
use pound "#method" for instance methods
-
use dot ".method" for class methods
class Article def summary #... end def self.latest #... end end # the spec... describe Article describe '#summary' #... end describe '.latest' #... end end
-
-
Use fabricators to create test objects.
-
Make heavy use of mocks and stubs
# mocking a model article = mock_model(Article) # stubbing a method Article.stub(:find).with(article.id).and_return(article)
-
When mocking a model, use the
as_null_object
method. It tells the output to listen only for messages we expect and ignore any other messages.article = mock_model(Article).as_null_object
-
Use
let
blocks instead ofbefore(:all)
blocks to create data for the spec examples.let
blocks get lazily evaluated.# use this: let(:article) { Fabricate(:article) } # ... instead of this: before(:each) { @article = Fabricate(:article) }
-
Use
subject
when possibledescribe Article do subject { Fabricate(:article) } it 'is not published on creation' do subject.should_not be_published end end
-
Use
specify
if possible. It is a synonym ofit
but is more readable when there is no docstring.# bad describe Article do before { @article = Fabricate(:article) } it 'is not published on creation' do @article.should_not be_published end end # good describe Article do let(:article) { Fabricate(:article) } specify { article.should_not be_published } end
-
Use
its
when possible# bad describe Article do subject { Fabricate(:article) } it 'has the current date as creation date' do subject.creation_date.should == Date.today end end # good describe Article do subject { Fabricate(:article) } its(:creation_date) { should == Date.today } end
-
The directory structure of the view specs
spec/views
matches the one inapp/views
. For example the specs for the views inapp/views/users
are placed inspec/views/users
. -
The naming convention for the view specs is adding
_spec.rb
to the view name, for example the view_form.html.haml
has a corresponding spec_form.html.haml_spec.rb
. -
spec_helper.rb
need to be required in each view spec file. -
The outer
describe
block uses the path to the view without theapp/views
part. This is used by therender
method when it is called without arguments.# spec/views/articles/new.html.haml_spec.rb require 'spec_helper' describe 'articles/new.html.haml' do # ... end
-
Always mock the models in the view specs. The purpose of the view is only to display information.
-
The method
assign
supplies the instance variables which the view uses and are supplied by the controller.# spec/views/articles/edit.html.haml_spec.rb describe 'articles/edit.html.haml' do it 'renders the form for a new article creation' do assign( :article, mock_model(Article).as_new_record.as_null_object ) render rendered.should have_selector('form', method: 'post', action: articles_path ) do |form| form.should have_selector('input', type: 'submit') end end
-
Prefer the capybara negative selectors over should_not with the positive.
# bad page.should_not have_selector('input', type: 'submit') page.should_not have_xpath('tr') # good page.should have_no_selector('input', type: 'submit') page.should have_no_xpath('tr')
-
When a view uses helper methods, these methods need to be stubbed. Stubbing the helper methods is done on the
template
object:# app/helpers/articles_helper.rb class ArticlesHelper def formatted_date(date) # ... end end # app/views/articles/show.html.haml = "Published at: #{formatted_date(@article.published_at)}" # spec/views/articles/show.html.haml_spec.rb describe 'articles/show.html.html' do it 'displays the formatted date of article publishing' article = mock_model(Article, published_at: Date.new(2012, 01, 01)) assign(:article, article) template.stub(:formatted_date).with(article.published_at).and_return '01.01.2012' render rendered.should have_content('Published at: 01.01.2012') end end
-
The helpers specs are separated from the view specs in the
spec/helpers
directory.
-
Mock the models and stub their methods. Testing the controller should not depend on the model creation.
-
Test only the behaviour the controller should be responsible about:
-
Execution of particular methods
-
Data returned from the action - assigns, etc.
-
Result from the action - template render, redirect, etc.
```Ruby # Example of a commonly used controller spec # spec/controllers/articles_controller_spec.rb # We are interested only in the actions the controller should perform # So we are mocking the model creation and stubbing its methods # And we concentrate only on the things the controller should do describe ArticlesController do # The model will be used in the specs for all methods of the controller let(:article) { mock_model(Article) } describe 'POST create' do before { Article.stub(:new).and_return(article) } it 'creates a new article with the given attributes' do Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) post :create, message: { title: 'The New Article Title' } end it 'saves the article' do article.should_receive(:save) post :create end it 'redirects to the Articles index' do article.stub(:save) post :create response.should redirect_to(action: 'index') end end end ```
-
-
Use context when the controller action has different behaviour depending on the received params.
# A classic example for use of contexts in a controller spec is creation or update when the object saves successfully or not. describe ArticlesController do let(:article) { mock_model(Article) } describe 'POST create' do before { Article.stub(:new).and_return(article) } it 'creates a new article with the given attributes' do Article.should_receive(:new).with(title: 'The New Article Title').and_return(article) post :create, article: { title: 'The New Article Title' } end it 'saves the article' do article.should_receive(:save) post :create end context 'when the article saves successfully' do before { article.stub(:save).and_return(true) } it 'sets a flash[:notice] message' do post :create flash[:notice].should eq('The article was saved successfully.') end it 'redirects to the Articles index' do post :create response.should redirect_to(action: 'index') end end context 'when the article fails to save' do before { article.stub(:save).and_return(false) } it 'assigns @article' do post :create assigns[:article].should be_eql(article) end it 're-renders the "new" template' do post :create response.should render_template('new') end end end end
-
Do not mock the models in their own specs.
-
Use fabrication to make real objects.
-
It is acceptable to mock other models or child objects.
-
Create the model for all examples in the spec to avoid duplication.
describe Article let(:article) { Fabricate(:article) } end
-
Add an example ensuring that the fabricated model is valid.
describe Article it 'is valid with valid attributes' do article.should be_valid end end
-
When testing validations, use
have(x).errors_on
to specify the attibute which should be validated. Usingbe_valid
does not guarantee that the problem is in the intended attribute.# bad describe '#title' it 'is required' do article.title = nil article.should_not be_valid end end # prefered describe '#title' it 'is required' do article.title = nil article.should have(1).error_on(:title) end end
-
Add a separate
describe
for each attribute which has validations.describe Article describe '#title' it 'is required' do article.title = nil article.should have(1).error_on(:title) end end end
-
When testing uniqueness of a model attribute, name the other object
another_object
.describe Article describe '#title' it 'is unique' do another_article = Fabricate.build(:article, title: article.title) article.should have(1).error_on(:title) end end end
- The model in the mailer spec should be mocked. The mailer should not depend on the model creation.
- The mailer spec should verify that:
-
the subject is correct
-
the receiver e-mail is correct
-
the e-mail is sent to the right e-mail address
-
the e-mail contains the required information
describe SubscriberMailer let(:subscriber) { mock_model(Subscription, email: '[email protected]', name: 'John Doe') } describe 'successful registration email' subject { SubscriptionMailer.successful_registration_email(subscriber) } its(:subject) { should == 'Successful Registration!' } its(:from) { should == ['info@your_site.com'] } its(:to) { should == [subscriber.email] } it 'contains the subscriber name' do subject.body.encoded.should match(subscriber.name) end end end
-
-
What we can test about an uploader is whether the images are resized correctly. Here is a sample spec of a carrierwave image uploader:
# rspec/uploaders/person_avatar_uploader_spec.rb require 'spec_helper' require 'carrierwave/test/matchers' describe PersonAvatarUploader do include CarrierWave::Test::Matchers # Enable images processing before executing the examples before(:all) do UserAvatarUploader.enable_processing = true end # Create a new uploader. The model is mocked as the uploading and resizing images does not depend on the model creation. before(:each) do @uploader = PersonAvatarUploader.new(mock_model(Person).as_null_object) @uploader.store!(File.open(path_to_file)) end # Disable images processing after executing the examples after(:all) do UserAvatarUploader.enable_processing = false end # Testing whether image is no larger than given dimensions context 'the default version' do it 'scales down an image to be no larger than 256 by 256 pixels' do @uploader.should be_no_larger_than(256, 256) end end # Testing whether image has the exact dimensions context 'the thumb version' do it 'scales down an image to be exactly 64 by 64 pixels' do @uploader.thumb.should have_dimensions(64, 64) end end end