#docking_station_spec.rb
require 'docking_station'
describe DockingStation do
let(:station) { DockingStation.new }
describe '#release_bike' do
it { is_expected.to respond_to :release_bike}
it 'releases working bikes' do
bike = Bike.new
station.dock(bike)
bike = station.release_bike
expect(bike).to be_working
end
it 'raises an error when empty' do
bike = Bike.new
expect { station.release_bike }.to raise_error("Docking Station is empty")
end
it 'should not release bikes if there are no bikes' do
expect { subject.release_bike }.to raise_error("Docking Station is empty")
end
end
describe '#dock' do
it { is_expected.to respond_to(:dock).with(1).argument }
it 'docks something' do
bike = Bike.new
subject.dock(bike)
expect(subject.bikes).to include bike
end
it 'raises an error if when full' do
station.capacity.times { station.dock Bike.new }
expect { station.dock Bike.new }.to raise_error("Docking Station is full")
end
end
it { is_expected.to respond_to(:bikes) }
describe 'initialisation' do
it 'should set the capacity when passed a number at initialisation' do
station = DockingStation.new(30)
30.times { station.dock Bike.new}
expect{ station.dock Bike.new }.to raise_error("Docking Station is full")
end
end
#docking_station.rb
require_relative 'bike'
class DockingStation
DEFAULT_CAPACITY = 20
attr_reader :bikes
attr_accessor :capacity
def initialize(capacity = DEFAULT_CAPACITY)
@bikes = []
@capacity = capacity
end
def release_bike
raise "Docking Station is empty" if empty?
@bikes.pop
end
def dock(bike)
raise "Docking Station is full" if full?
@bikes << bike
end
private
def full?
return @bikes.length >= capacity
end
def empty?
@bikes.empty?
end
end
#bike.rb
class Bike
def working?
true
end
end
RSpec
Using let helper method
Use let to define values to use in tests. The value will be cached across multiple calls in the same example but not across examples. So what that means is that if you call let multiple times in the same block, it evaluates the let block just once (the first time it’s called). When you call it again in the second it block, the let helper method will get re-evaluated again.
describe DockingStation do
let(:station) { DockingStation.new }
describe '#release_bike' do
it 'releases working bikes' do
bike = Bike.new
station.dock(bike)
bike = station.release_bike
expect(bike).to be_working
end
end
Predicate Matchers
For methods that return true or false, there is a convention in Ruby to end the method name with a ? RSpec provides a shorthand syntax, so the test expectation can be expressed way more concisely.
RSpec supports a one-liner syntax for setting an expectation on the subject.
it { is_expected.to respond_to :release_bike}
is similarly defined as expect(subject).to to respond_to...
describe DockingStation do
it 'responds to release_bike' do
expect(subject).to respond_to :release_bike
end
end
# TO
describe DockingStation do
let(:station) { DockingStation.new }
describe '#release_bike' do
it { is_expected.to respond_to :release_bike}
end
end
Another example
describe '#metropolis?' do
context 'without population' do
it 'is false' do
expect(subject.metropolis?).to be false
end
end
# TO
describe '#metropolis?' do
context 'without population' do
it { is_expected.to_not be_metropolis }
end
end
Before and After hooks
They provide a way to define and run the setup. The before(:each) method is where we define the setup code. When you pass the :each argument, you are instructing the before method to run before each example in your Example Group i.e. the it block inside the describe #land in the code below.
In the line airport.land(plane)
we are calling the land method on the airport class and passing in the plane. This way the code is cleaner in our tests and we do not need to have to add it each it block.
describe Airport do
let(:airport) { Airport.new }
let(:airport) { Plane.new }
describe '#land' do
it { is_expected.to respond_to(:land) }
before(:each) do
airport.land(plane)
end
it 'lands a plane and puts it in the hangar' do
expect(airport.hangar).to include plane
end
end
before(:all)
- runs before all examples
after(:each)
- runs after each example
after(:all)
- runs after all examples
expect {} and expect ()
expect{}
will tell expect to observe the whole process while expect()
will evaluate inside the () first and check if the return value matches the matchers following.
If we want to just check the return value then we can simply use the expect(). The argument is evaluated immediately. Otherwise if we want to check for any changes we can use the expect{}, with this we can pass a code block inside. The block contents isn't executed immediately, it's execution is determined by the method you're calling.
The expect statement below will actually equal this:
expect do
25.times {airport.land(plane}
end.to raise_error('Airport is full')
it 'prevents landing if the airport is full' do
expect { 25.times { airport.land(plane) } }.to raise_error('Airport is full')
end
RSpec syntax
respond_to
You're asserting that the Airport class when instantiated that the land method returns true, which means it is present inside the Airport class.
describe Airport do
let(:airport) { Airport.new }
it { is_expected.to respond_to(:land) }
end
be_
We can use the 'be' matcher to match whether the specified object is true/false/nil
expect(obj).to be_truthy # passes if obj is truthy (not nil or false)
expect(obj).to be_falsey # passes if obj is falsy (nil or false)
expect(obj).to be_nil # passes if obj is nil
expect(obj).to be # passes if obj is truthy (not nil or false)
raise_error
Use the raise_error matcher to specify that a block of code raises an error. You can also use the raise_exception which will both work the same.
it 'should not release bikes if there are no bikes' do
expect { subject.release_bike }.to raise_error("Docking Station is empty")
end
with()
Use with to specify the expected arguments. A message expectation constrained by with will only be satisfied when called with matching arguments.
with(1, true)
=begin
will match with foo(1, true)
=end
include
Can use the include matcher to specify that a collection includes one or more expected objects. It succeeds if any object of the given collection passes the specified matcher. This works on any object that responds to #include? (such as a string or array):
Below we expect the subject.bikes (which is stored as an array) to include the bike that was docked.
it 'docks something' do
bike = Bike.new
subject.dock(bike)
expect(subject.bikes).to include bike
end
Ruby
Initialising the object and Setting Instance Variables
Set an attribute on an instantiated Object using an @
instance variable
Whenever you call the method new on a class, as in Person.new, the class will create a new instance of itself. It will then, internally, call the method initialize on the new object. Doing so it will pass all the arguments that you passed to new on to the method initialize.
class Person
def initialize(name)
@name = name
end
end
Person.new("Ada")
The string "Ada" will be passed on to our initialize method, and end up being assigned to the local variable name.
The body of the initialize method assigns the value of the local variable name to an instance variable @name. The instance variable of @name will be accessible in any instance method in a particular instance of a class, they are visible everywhere in the object, that is, in every method that the object has.
attr_reader and attr_accessor
Use attr_reader to read the data on an object.
We cannot get the data, the name from the person object because it’s private. So to make it publicly available, we need a method. Methods in Ruby are public by default.
class Person
def initialize(name)
@name = name
end
def name
@name
end
end
attr_reader
does exactly the same thing, only now you can shorten that code like so.
class Person
attr_reader :name
def initialize(name)
@name = name
end
end
attr_accessor
lets you read and change the data on an object and in the background provides a getter and a setter method.
class Person
attr_accessor :name, :age, :sex, :email
def initialize(name)
@name = name
end
end
john = Person.new("John")
john.name = "Jim"
puts john.name # => Jim
Initializing a default value and using a constant variable
The DockingStation
class has an initialize method with two instance variables, bikes and capacity. It has parameters which outlines the default values set to 20. When we create an instance of the DockingStation class, the initialize method is automatically called.We can see that the capacity is set to a constant variable which is outlined in uppercase as DEFAULT_CAPACITY = 20
.
class DockingStation
DEFAULT_CAPACITY = 20
attr_reader :bikes
attr_accessor :capacity
def initialize(capacity = DEFAULT_CAPACITY)
@bikes = []
@capacity = capacity
end
Ruby Constants
We can use :: as a Namespace operator: we use it to access the DEFAULT_CAPACITY constant defined within the DockingStation class.
it 'should not let the bike dock if storing more than 20 bikes' do
expect { DockingStation::DEFAULT_CAPACITY.times do
subject.dock Bike.new
end }.to raise_error("Docking Station is full")
end
class DockingStation
DEFAULT_CAPACITY = 20
attr_reader :bikes
def initialize
@bikes = []
end
def dock(bike)
if full?
raise "Docking Station is full"
else
@bikes << bike
end
end
private
def full?
return @bikes.length >= DEFAULT_CAPACITY
end
end
require and require_relative
require
is a method that is used when you want to reference and execute code that is not written in your current file. The method takes in a path in the form of a string as an argument and there are two ways the string can be formatted — either as an absolute path or a shortened name.
# absolute path
require './app/example_file.rb'
# shortened name
require 'example_file'
The shortened strings always depend on the listed directories in $LOAD_PATH. $: is a global variable that is used for looking up external files. You may see this used in your bin/environment.rb like this:
bin/environment.rb
$LOAD_PATH << './app'
require
is usually called when requiring gems loaded in gemfile. Though require can be used to both execute gems and external dependencies, the preferable method to load relative paths is require_relative
, it's a subset of require and is a convenient method to use when you are referring to a file that is relative to the current file you are working on (basically, within the same project directory).
Private methods
Classes sometimes want to keep certain methods private: methods that aren’t supposed to be called from outside of the object. Only the object itself is supposed to use them internally, from other methods. By default all methods are public!
Private methods call ONLY be called from inside the class where it’s defined.
We can declare private methods by simply typing private
and any methods below private will all be private methods.
def dock(bike)
raise "Docking Station is full" if full?
@bikes << bike
end
private
def full?
return @bikes.length >= capacity
end
A user will not need to check if the docking station is full. A user just needs to know if they can dock the bike. So internally we can create a private method to check if the docking station is full before docking their bike.
fail/raise expectations
Signal exceptions using the fail method.
Use raise only when catching an exception and re-raising it (because you'tr purposefully raising an exception).
begin
fail 'Oops'
rescue => error
raise if error.message != 'Oops'
end
# Explicit
raise RuntimeError.new("You messed up!")
# ...produces the same result
raise RuntimeError, "You messed up!"
# ...produces the same result. But you can only raise
# RuntimeErrors this way
raise "You messed up!"
Guard clauses
Guard clauses are an awesome way to make a piece of code more succinct and understandable.
def my_method(variable)
if variable == 'great'
# do something great
else
return nil
end
end
# TO
def my_method(variable)
return nil unless variable == 'great'
# do something great
end