Testing your code: Examples from my command line project

Posted by William Dewey on June 3, 2021

In my last blog post, I explained how to test your Ruby code with RSpec. In this post, I will provide an example of testing in action, based on my Ruby command line project Music Explorer (which interfaces with the Spotify API to browse music artists and associated information). This was the first project for Flatiron School and I chose it to demonstrate testing because it is a relatively simple application, using a single language Ruby, and not incorporating a complex frameworks like Rails.

After you have installed Rspec (see my last post), it is best to write a separate .spec file for each class. This is not a requirement but helps to organize your test code and the output better. My project had the bulk of its logic in the CLI, Artist and API classes. The CLI provides the text-based command line interface, the Artist class contains data about artists (retrived from the API), and the API class sends data back and forth from the external API.

Testing a class

I will focus on my Artist class because it is the simplest class to test, with the fewest methods.

class MusicExplorer::Artist

  attr_accessor :name, :top_tracks, :albums, :genres, :related_artists

  @@all = []
  
  def self.create_artist(artist_query)
    artist_data = lookup_artist(artist_query)
    self.new(artist_data)
  end

  def self.lookup_artist(artist_query)
    #Calls the API with the user's query, in order to get a hash of artist data
    
    api = MusicExplorer::API.new(artist_query)
    api.retrieve_artist_data
  end

  def self.all
    @@all
  end

  def initialize(artist_data = nil)
    #creates Artist object from the hash returned from the API
    if artist_data && artist_data.length > 0
      @name = artist_data[:name]
      @genres = artist_data[:genres]
      @top_tracks = artist_data[:top_tracks]
      @albums = artist_data[:albums]
      @related_artists = artist_data[:related_artists]
      @@all << self
    end
  end

end

The core functionality is to take in the name of an artist, and return an artist object with data (after calling the API). New objects will be initialized with a hash of data returned from the API.

How might we test this code? First we need to require all the files and provide a container for the test, in the artist_spec.rb file:

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

RSpec.describe "MusicExplorer::Artist" do
end

Testing the self.create_artist function

Let’s first test the create_artist class method, which is intended to return an artist object from a user query. Within the context of creating an artist, we want to make sure an artist is actually returned. So first we need to call the method with a sample artist query, “Bob Dylan.” We are just setting up the data at this point, before writing tests:

context "when creating an artist" do
    artist_query = "Bob Dylan"
    MusicExplorer::Artist.create_artist(artist_query)
	end

Then we run the actual test, making sure that a new artist has been created (by searching the Artist.all array for an object with a name attribute equal to Bob Dylan):

it "creates a new artist" do
      expect(MusicExplorer::Artist.all.find { |artist| artist.name == "Bob Dylan" }).not_to be nil
    end

You can do any complicated query you like within the expect function, but then I just made sure it wasn’t returning nil, which is what would happen if find failed. As it happens, all the tests pass.

Testing self.lookup_artist

By testing self.create_artist, we are implicitly testing self.lookup_artist and self.all. If those methods fail, the test will also fail. But we can and should test self.lookup_artist separately, as that will help isolate problems to that class method if it was the source of them:

  context "when looking up an artist" do
    artist_query = "Bob Dylan"
    artist_data = MusicExplorer::Artist.lookup_artist(artist_query)
    it "returns artist data in a hash" do
      expect(artist_data).not_to be nil
      expect(artist_data).to be_a(Hash)
    end
    it "returns the correct artist" do
      expect(artist_data[:name]).to eq("Bob Dylan")
    end
  end

Again, we set up the data and call the desired method, this time assigning the return value to a variable artist_data. Then we make sure that artist_data first of all exists, is not nil, and then, using the be_a method, has the correct data type of Hash. Finally, we use the method eq, testing for equality, to make sure that “Bob Dylan” is stored under the :name key of hash.

Testing initialize

The last method to test is initialize, the method that automatically gets called with Artist.new. This is a case where it is helpful to create fake data in order to test the method in isolation:

artist_data = {name: "Beyonce", genres: [], top_tracks: [], albums: [], related_artists: []}
  artist = MusicExplorer::Artist.new(artist_data)
  context "when initialized with artist data" do

  end

In reality, the artist data hash would be more complicated, but this is enough to test whether an Artist instance gets initialized with all the correct values. (I created more specs in the API class, beyond the scope of this post). Then we test that all the attributes of the artist, and the class variable Artist.all, were initialized properly:

it "is an artist instance" do
      expect(artist).to be_an_instance_of(MusicExplorer::Artist)
    end
		
it "has a name" do
      expect(artist.name).to eq "Beyonce"
    end

    it "has genres" do
      expect(artist.genres).not_to be nil
      expect(artist.genres).to be_a(Array)
    end
#repeat for the other attributes that should be arrays

    it "is included in all artists" do
      expect(MusicExplorer::Artist.all).to include artist
			
    end

Most of this has been seen before, but two new methods are to be an instance of, helpful to make sure instances are being initialized correctly and include which tests if an element is present in a collection.

This is just the beginning of testing. There are many more methods and scenarios to consider when testing your code. For these reasons, it takes a while to become a proficient tester, and I am very much in the learning process.

References Music Explorer project