ruby learning tdd testing instance variables predicates

Recap And Overview

Learning Ruby With Tests

  • Intro: motivation and overview
  • : class, method, if … else and a test system
  • Part 2: predicates, variable scopes, initialize

In the 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