ruby learning tdd testing instance variables predicates
Recap And Overview
Learning Ruby With Tests
In the first part we created
a simple class name Vehicle
and a very basic method to test it. We also touched
decision making using an if … else statement. We have come across the def
keyword which defines a method.
In this part we will expand on the class concept: we will learn how to use it as a container for a state using instance variables and how read/modify such a state. We will also improve our way to test our code by adding a more specific test method.
Let's Get Moving
Right now our vehicle can't do much: it stands still, meaning its velocity
method always returns 0. I think it is okay that we start with a velocity of 0 when
we create a Vehicle
instance, but we also want a method to actually move
forward. Let's call that method accelerate
, but let's also write a test
for it first:
# Current Vehicle and assert method implementation omitted
# Requires a method named 'accelerate'
vehicle = Vehicle.new
assert vehicle.respond_to? :accelerate
The assert
statement looks a bit different. In fact, there's a lot
in this line so let's have a look at it. We know our assert
method
requires an argument, something that evaluates to true or false and the code
following the assert
is our argument. It's a method call itself,
returning something that will be passed as input for assert
.
Interlude: Predicates
What is that respond_to followed by a question mark? It's a method
that works on every object in Ruby. Again and just like the new
method we called on Vehicle
, we didn't define it. It's part of the
inheritance that's been briefly mentioned in Part 1 and for now it's safe to
just accept that every object in Ruby has it.
What is it good for? When you think of an object's methods as messages you can send them,
respond_to?
is a message that checks whether you can safely send
that message to the object it's called on.
Methods that end with a question mark are called predicates –
like those you may know from your Logic
class. The question mark does nothing magical, it's a legal character to
include in your method's name. The convention, however, is that those methods
return a boolean value, i.e. either true
or false
.
Interlude: Parentheses
It may be a good time to look at the assert line from another perspective: what
gets evaluated first? You know from maths that you calculate things in
parentheses first, working your way from the inside out. Ruby is no different
here, but in most cases parentheses are optional. The following statement
is synonymous to the assert
statement above:
assert( vehicle.respond_to?(:accelerate) )
This may look easier to wrap one's head around how Ruby evaluates it, but there are many programmers (myself included) who would rather leave parantheses when they're not required.
Anyway, we still need to get this new test passing (run your Ruby file to see
it failing first). Being the good TDD citizens we are, we only add the minimal
amount of code to make our tests happy:
# File: crash_test.rb
class Vehicle
def velocity
0 # A method returns its last statement
end
def accelerate
# Does nothing as of now
end
end
# Current implemention of assert omitted
# Expects a fresh Vehicle instance to have a velocity of 0
vehicle = Vehicle.new
assert vehicle.velocity == 0
# Requires a method named 'accelerate'
vehicle = Vehicle.new
assert vehicle.respond_to? :accelerate
Running the file again will get you two dots on your terminal:
ruby crash_test.rb
Equality Test
The mere existing for an accelerate
method doesn't suffice. Let's
assume that a call to accelerate
will increase the vehicle's velocity
by 1 unit (whether that is mph or kmh is up to you). Here's the test:
# Increase the velocity by one
vehicle = Vehicle.new
vehicle.accelerate
assert vehicle.velocity == 1
Interlude: Basic Test Pattern
This test illustrates the pattern of a test almost perfectly. It consists of three parts: Setup, Run, Check. They don't have to be only one line and oftentimes you can even collapse some or all of these into a one-liner. But the basic pattern remains: you build the world into which you want to place your test, you call the method that is to change that world or supposed to return information about it and finally you check whether everything has worked out as intended.
When you run the test, you'll see it failing, but you don't know why. Sure, we see that the equality check in our last test fails, but it would be nice to get a little more insight about the actual values involved. I suggest we create an assert method that specifically checks for equality of two objects and outputs more than just an 'F' for a failing test.
def assert_equal expected, actual
if expected == actual
puts '.'
else
puts "F - Expected #{expected.inspect}, got #{actual.inspect}."
end
end
The "#{variable}"
format is a string insertion: if you have a
string in double quotes, you can insert the string-representation of a
variable's content right into it. The inspect
method is
a great way to debug the state of the object you call it on.
Our new test method will give us detailed information about the failing
condition and we can modify our existing tests to use
assert_equal
:
# Expects a fresh Vehicle instance to have a velocity of 0
vehicle = Vehicle.new
assert_equal 0, vehicle.velocity # changed to use assert_equal
# Requires a method named 'accelerate'
vehicle = Vehicle.new
assert vehicle.respond_to? :accelerate
# Increase the velocity by one
vehicle = Vehicle.new
vehicle.accelerate
assert_equal 1, vehicle.velocity # changed to use assert_equal
Adding State
To make our failing test pass, we need to have something that preserves a state
for our instance and the way to approach this is using a variable.
You have come across a variable already: the lower-case vehicle
is a
local variable that gets assigned the return value of
Vehicle.new
(which is a brand new instance of Vehicle).
As the keyword local suggests, variables come with different scopes. A local variable is only valid for a local context, for example a single method. In our code example we couldn't use the variable in the Vehicle instance. We could, however, create another variable with the exact same name, they wouldn't overwrite each other. At the same time, they wouldn't share their bindings – they're completely isolated.
Our Vehicle's velocity is a state information that should be available
throughout the entire instance, not just a single method. Instance
variables exist for this purpose and, unsurprisingly when you look at the
name, their scope is the instance they're defined in. Instance variables are
easily recognizable: you preceed them with an »@«:
@my_instance_variable_name
.
The changes we need to make in our Vehicle class are twofold: first, we need
to make sure that velocity
doesn't statically return 0, but the
actual velocity - the content of an instance variable - instead. Secondly,
we want to increase the value stored in that instance variable for each
call on accelerate
.
class Vehicle
def velocity
@velocity
end
def accelerate
end
end
With the change in line 3, we get a failing test for something that was passing before. This is a very important thing: an existing test reminds us that we've broken something. When your changes go really deep, you won't get around changing your tests, but our case is really simple and we won't touch the now failing test at all. Instead, we need to fix it in our object under test: the Vehicle class.
Our test tells us that where we expected the velocity of a newly instantiated
vehicle to be 0, we now get nil
. We've met nil
in
part 1 before where it was the return value of a method whose method body
hasn't been implemented yet. nil
is also the default value for
an instance variable that has yet to be assigned a value.
Default Value
There are two ways to go about vehicle.velocity
returning the expected
0 again. The first is pretty easy: we use a logical OR. In Ruby, everything
that is not nil
or false
will be treated as true
,
or truey to be more precise.
class Vehicle
def velocity
@velocity || 0 # OR operator
end
def accelerate
end
end
The @velocity || 0
means: if @velocity
has a value other
than false
or nil
, return that one. Return 0 otherwise.
If you run the test, you'll see that we're passing again. Yay!
There is a better, safer way to approach this though. The above version actually doesn't set the instance variable, the method that returns its value only conveniently returns a sane default value. What if we could set certain instance variables right away when the instance is created?
The Initialize Method
Remember the ominous new
method that creates instances? This method
calls a method called initialize
on the newly created instance, passing
all parameters the call to new
got on to initialize
.
A stock initialize
method is »magically« defined for all objects in Ruby
objects, but you can define your own as well. This mechanism is called overriding
and it's a very important thing when dealing with inheritance in object oriented
languages. Let us override the initialize
method someone else defined for us:
class Vehicle
def initialize
@velocity = 0 # Set @velocity right away
end
def velocity
@velocity # We can rely on @velocity havng a start value now
end
def accelerate
end
end
Don't worry about the initialize
method too much at this point.
For now it's sufficient to know that a method called initialize
is
the very first method that gets called when an instance is spawned. What's more
important: running the tests shows us that our little refactoring
didn't break anything from the previous iteration and we're only one failing
test away.
Accelerating
Implementing accelerate
is almost trivial now: we can rely on
the fact that @velocity
has a meaningful default value: 0.
To make the test pass, we only need to increase @velocity
by 1:
class Vehicle
def initialize
@velocity = 0
end
def velocity
@velocity
end
def accelerate
@velocity = @velocity + 1
end
end
Running the tests will get you three nice dots already which means we're
done. Only one minor cosmetic modification: @velocity += 1
is the short form of new value = old value + something.
So with this changed, behold the final version of our crash_test.rb file
for this chapter:
# File: crash_test.rb
class Vehicle
def initialize
@velocity = 0
end
def velocity
@velocity
end
def accelerate
@velocity += 1
end
end
def assert expression
if expression
puts '.'
else
puts 'F'
end
end
def assert_equal expected, actual
if expected == actual
puts '.'
else
puts "F - Expected #{expected.inspect}, got #{actual.inspect}."
end
end
# Expects a fresh Vehicle instance to have a velocity of 0
vehicle = Vehicle.new
assert_equal 0, vehicle.velocity
# Requires a method named 'accelerate'
vehicle = Vehicle.new
assert vehicle.respond_to? :accelerate
# Increase the velocity by one
vehicle = Vehicle.new
vehicle.accelerate
assert_equal 1, vehicle.velocity
Summary And Outlook
There was a lot in this part: predicates, Ruby and its parenthesis, a new test
method assert_equal
(now outputting status info for test failures),
(instance) variables, variable scopes and the initialize
method.
In part 3 we will have a first bash at this illustrious concept of inheritance you've been hearing so much about already, we'll organise our code a bit by using seperate files and we'll learn about optional parameters for a method. Oh, and we'll further expand on the Vehicle class of course.
comments powered by Disqus