Testing your code

Posted by William Dewey on May 27, 2021

Most of programming is about getting your code to work properly. New programmers often just write a bunch of code, play with while running the code, and then try to find the bugs (perhaps using some debugging tools in the process). But this is a haphazard approach, and it can miss some edge cases, or result in complex bugs that is difficult to trace to a specific piece of code. A better approach is to write unit tests using a testing suite, testing each function and method to make sure it works as expected. (Even better is test driven development: write the tests first, and then write code to make them work. This post, however, is focused on how to write tests, and I will save TDD for a future post.) Testing not only makes debugging simpler and more effective, it is also a skill highly sought after by employers in the tech field, something you often need to know to get a developer job.

Testing with RSpec

Each popular language has testing tools designed for it, and for Ruby the most popular testing suite is RSpec. In order to install it, navigate to your project directory in terminal and type gem install rspec. This will create three files:

	create  .rspec
	create  spec
	create  spec/spec_helper.rb

In the case of a Rails project, you would type gem install rails and it would also create spec/rails_helper.rb. You can then write tests and they go in the spec folder. To run the tests, simply type rspec (or if that gives an error, bundle exec rspec).

At the top of the spec file, you need to require respec and spec helper, in addition to the ruby scripts you are testing (I am using the example of my Music Explorer CLI project, and “./lib/music_explorer” is the base file that defines the CLI module and requires all the necessary files).

require "rspec"
require "spec_helper"
require "./lib/music_explorer"

RSpec syntax

RSpec syntax is designed to be easily readable by developers. The basic syntax of RSpec starts with the describe method, which identifies the class or instance it is testing, like this:

describe User do

end

or this:

describe 'Some string' do

end

Or for a specific method, you preface the string with # for a class method and . for an instance method:

describe Agent, '#favorite_gadget' do
  ...
end
describe Agent, '#favorite_gadget' do
  ...
end

For an individual situation that you’re testing, you use the it keyword followed by a descriptive string

RSpec.describe Artist do
    it "has a name" do
    end
end

To group your tests in a more organized way, you can specify a context:

RSpec.describe "MusicExplorer::Artist" do
  context "when initialized with artist data" do
    it "has a name" do
    end

    it "has genres" do
    end
	end
end

All of this is merely semantic, for easy to read test code and output (all the strings will be visible in the output).

The actual test code starts with the keyword expect:

it "has genres" do
      expect(artist.genres).not_to be nil
    end

Four phases of testing

The four phases of testing your code are setup, exercise, verification, and teardown.

Setup means preparing the data. In order to test your code, for example, you may need to make instances of the class. Here I am creating an artist instance to test my Artist class:

RSpec.describe "MusicExplorer::Artist" do
  artist_data = {name: "Beyonce", genres: [], top_tracks: [], albums: [], related_artists: []}
  artist = MusicExplorer::Artist.new(artist_data)
	...
end

Exercise means running the code you want to test:

context "when initialized with artist data" do
    it "has a name" do
      name = artist.name
    end

Specifically, I am testing the name attribute, with name = artist.name being the exercise code.

Verification is what tests the resulting code against the expectation, starting with the expect keyword. If it passes the test will be green when you run rspec, otherwise the test will be red and produce an error:

it "has a name" do
      name = artist.name
			expect(name).to eq "Beyonce"
    end
end

Often, you can combine exercise and verification into one line, for simple tests:

it "has a name" do
      expect(artist.name).to eq "Beyonce"
end

Teardown is handled by the testing framework itself, you do not usually need to write teardown code for basic tests. RSpec automatically resets state after a testing block ends, so that the code can be tested in a different way.

Examples of expectations

There are a number of expectations that can be tested with the expect method, as you can see above. Expect takes in a parameter, the value you are testing. The .to method determines what the expectation is, the condition you want to be true for the test to pass. If you want it to be false, use .not_to instead. The following is just a sample of possible expectations:

.to eq tests for equality with a particular value, you can see this in many of the tests above.

.to be_truthy tests for a truthy value, either the boolean value true or another value that is not nil (anything other than nil or false is truthy in Ruby). .to be_true tests for boolean true. The opposite is .to be_falsy for falsy values or .to be_false for false values.

.to be_nil test for equality with nil.

.to match() matches against a regex expression, for instance:

expect(agent.number.to_i).to match(/\d{3}/)

You can use simple comparison operators like > < expect(agent.kill_count).to be >= 25

.to be_a, to be_an, .to be_an_instance_of are used to test for classes and instances, very useful for object oriented code:


.to raise_error tests to see if the code raises an error, which is often the expected and desired behavior.

References

Better Specs RSpec testing for beginners, part 1 RSpec testing for beginners, part 2