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:
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:
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:
Running the file again will get you two dots on your terminal:
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:
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.
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
:
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
.
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.
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:
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:
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:
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