-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Common issues
def initialize
@capacity = 6
end
6
is a numeric literal and its purpose in this statement is unclear. Encapsulate in a constant:
DEFAULT_CAPACITY = 6
def initialize
@capacity = DEFAULT_CAPACITY
end
Each time a string literal (e.g. 'flying'
) is interpreted by Ruby, a new string object is created in memory. Therefore, every time a method is called that contains a string literal (e.g. 'sunny'
) a new object is created. This can lead to lots of unnecessary objects being created when we're not interested in the object identity of a string, just its value. To overcome this, use symbols instead e.g.: :flying
, :sunny
.
Methods should be either commands or queries. As a general rule:
- Command methods should start with a verb: what does the method do?
- Query methods should be a nounal.
- Avoid naming query methods with a verb.
- Avoid naming command methods with a noun.
- Avoid depending on the return value of a command method (this rule is often broken!).
- Never have query methods that alter program state.
attr_reader :capacity
def full?
planes.count >= @capacity
end
Prefer delegating to the reader method (planes.count >= capacity
) if it is defined, instead of accessing the instance variable.
Instance variables are initialized when first assigned. The following is therefore redundant:
def initialize
@name
end
And:
def initialize
@name = nil
end
if stormy?
fail 'Bad weather'
elsif full?
fail 'Airport full'
else
...
end
prefer separate methods for validation, or use guard clauses:
fail 'Bad weather' if stormy?
fail 'Airport full' if full?
...
class Plane
attr_accessor :landed
def land
landed = true
end
end
Which is the correct use of an instance of Plane
?
plane.landed = true
or
plane.land
Prefer the custom method (land
) for more control over the value of @landed
and use attr_reader
instead.
This is a Java convention, but is disliked in Ruby. Use attr_*
conventions instead: status
over get_status
and status=
over set_status
.
def stormy?
weather == :stormy ? true : false
end
weather == :stormy
is already a boolean expression, so the ternary is redundant. Use ternaries when you want to return values other than true
and false
. Otherwise, just return the boolean:
def stormy?
weather == :stormy
end
describe Plane do
let(:plane) { Plane.new }
it 'can land'
plane.land
expect(plane).to be_landed
end
end
Is the same as:
describe Plane do
it 'can land'
subject.land
expect(subject).to be_landed
end
end
Or, use a named subject:
describe Plane do
subject(:plane) { Plane.new }
it 'can land'
plane.land
expect(plane).to be_landed
end
end
plane = double :plane
allow(plane).to receive(:landed?) { true }
Use the block if you need to give the landed?
stub some code, otherwise prefer:
plane = double :plane
allow(plane).to receive(:landed?).and_return true
Or use the shortcut syntax:
plane = double :plane, landed?: true
describe Airport do
let(:plane) { double :plane }
it 'can land planes' do
allow(plane).to receive(:land)
subject.land plane
expect(subject.planes).to include plane
end
end
This only tests half of the requirements (depending on your design). We are not testing that the land
method of plane
is called:
describe Airport do
let(:plane) { double :plane }
it 'can land planes' do
expect(plane).to receive(:land)
subject.land plane
expect(subject.planes).to include plane
end
end
Does every implementation in your code have associated unit tests? For example, if you take off a plane:
def take_off(plane)
...
end
What if the airport has multiple planes? Do you test that the correct plane is no longer in the airport?
In the grand finale, don't mock out the weather so that it is never stormy. Try to find ways to simulate a real run, but in such a way that your tests are not adversely affected by random behaviour. Ideally, this should be refactored into two separate tests.