Automate your testing with Ruby: Hands-on

In my last blog post, I suggested that many early-stage coders, when faced with the the difficulties of learning  a test suite, will often abandon automated testing entirely. I suggested that there was a better option - one where such developers could improve their knowledge of the core language, while simultaneously learning important testing skills.

Here I will elaborate on that idea.

Below is a simple Person class. It has an attribute accessor that will allow the person's first name to be set and retrieved. It also has a class contant PEOPLE that will retain  all new instantiations of Person in an array.

It also has an instance method that returns 'hi,' and a class method that returns all person instances:

class Person

  attr_accessor :first_name

  PEOPLE = []

  def initialize(first_name)
    @first_name = first_name
    PEOPLE << self
  end

  def return_hi
    'hi'
  end

  def self.all
    PEOPLE
  end

end

 

First step:  Let's instantiate a couple of instances to run our tests on:

david = Person.new('David')
rachel = Person.new('Rachel')

Now that we have our instances, we can begin our tests. But what should we test for?

Generally speaking, there are two types of errors that you will test for when coding:

  1. Syntax errors - invalid code that the interpreter does not understand.
  2. Semantic errors (sometimes referred to as 'logical errors'). These are valid code that will work, however it does not do what you want.

 

Let's start by examining how Ruby treats syntax errors:

Syntax error example
How Ruby treats a syntax error


Ruby immediately notices that there's a syntax error.

What happens if we create a new class with a syntax error?

Error in Ruby class
A syntax error in a class

Ruby catches this too.

What about a method?

Method error
Aye aye captain

Huh? Apparently Ruby is fine this.

This brings us to a very important point: Ruby will read code when loaded, but it will not read methods until they are called.

To demonstrate, here is what appears when the method is called:

Demo of error  upon method cal
Calling the method

What this means in practical terms, is that you should call all methods in your test.

Let's begin by copying and pasting our methods:

return_hi

self.all

Notice that the first method is an instance method. We can call it on one of the objects we instantiated above. The second method is a class method. Obviously outside the class definition we will need to use the class name, rather than self.

david.return_hi

Person.all

Even without looking at the return values, these will be enough to trigger syntax error should any exist in those methods.

Checking for semantic errors is a little more time-consuming as it requires informing Ruby of the expected result. Luckily, you probably know enough Ruby methods to create your own tests. I've included a number of different types of test, and you can refer to my previous blog post for more examples:

Here, we are testing the PEOPLE constant, and each instance of the Person object:

raise "not all person objects are in PEOPLE"  unless Person.all.length == 2
raise "person objects are not all instances of Person" unless Person.all.all? {|person_obj| person_obj.instance_of?(Person) "

 

Type Testing

 

To check that our new instantiations are in fact objects of the Person class, there are a number of different methods that can be called on them. Here I will use the ternary operator to return 'pass' if the result is as expected, or 'false' otherwise.

david.is_a?(Person) ? 'pass' : 'fail'     # pass
rachel.kind_of?(Person) ? 'pass' : 'fail'     # pass
david.instance_of?(Person) ? 'pass' : 'fail'     # pass

Note that is_a? is an alias of kind_of? However, instance_of? is different in that it checks that the objects is an instance of the exact class passed as an argument, and not a superclass. You can see below that instance_of? fails unless the class that instantiated the object is passed in.

david.is_a?(Object)    # true
rachel.kind_of?(Object)   # true
david.instance_of?(Object)     # false

 

Duck Typing

duck typing

 

In the examples above, we tested the type of objects being instantiated by looked at their type. However, Ruby doesn't really care about what type the objects is, what it really cares about is what methods and properties the object has.

This is known as "duck typing"  and it comes from the phrase "if it walks like a duck and quacks like a duck, then it must be a duck."

We can check what methods the class and its instances have in the following ways:

Person.methods(false)     # [:all] 
Person.instance_methods(false)     # [:first_name, :return_hi, :first_name=]

Then, we can test that our objects respond to the correct methods:

bob.respond_to?('return_hi') ? 'pass' : 'fail'    # pass
rachel.respond_to?('first_name') ? 'pass' : 'fail'    # pass

 

What's next?

You'll be glad to know that if you're capable of understanding the methods and return values above, you are already most of the way towards testing your code with a test-suite.

Minitest is a test-suite that supports creating tests using Ruby methods similar to those demonstrated above. It also adds support assert statements.

Can you convert the above tests to Minitest?