WWJD: Strategies for Debugging

When it comes to the sometimes extreme complexity of relationships created by object-oriented programming, we all know that heart-sinking feeling when one little change causes a lot of things to break. Or, when looking for the culprit behind the error in your code feels like a unicode version of 'Where's Waldo.'

via Giphy

At times like this, one of the very best things you can do is to stop looking at the big picture, enter into what you've created, and troubleshoot for yourself where things have gone wrong, line by line of code.

While I bear no religious affliation, for the intents and purposes of this metaphor, let's take ourselves into one of the most ubiquitous stories of modern civilization and imagine, if you will, that you're the New Testament Judeo-Christian God.

via Disney Blogs

You've created a fabulous creature called a human, they love you (mostly) and you love them (mostly). Everything is going well (kind of).

But sometimes, things get complicated between the humans you've created, the humans they've procreated, and all of that ensuing sin.

Unlike Old Testament Judeo-Christian God, you know the most direct way to solve this problem is to get a little hands-on. But it's the New Testament now. It's been nice trying to figure out the situation from a safe distance. But sometimes, when you don't put yourself into the shoes of your object, you can't see what's going on.

To do so, we must, stop everything at one line, find a way to infiltrate into the world we've created, and figure that #$%& out from the bottom-up. Enter Pry, aka Jesus.

As New Testament God's alternative to destroying everything (DELETE ALL AND START AGAIN), he's decided to demonstrate emotional growth, by tasking Jesus with coming to Earth, living amongst the humans and debugging their greatest moral problems.

Courtesy of A Science Enthusiast

Here's where this tenuous religious metaphor ends, and where I begin presenting a few strategies for debugging with tools like Pry (Jesus) that I've used and researched, that may help you to slowly back away from the wall you've been banging your head against, and expedite your journey to testing success.

Step 1: Read the Error

BEFORE YOU TRY ANYTHING - First and foremost, always read the error. I mean, really, read it. Don't just skim over it, no matter how tired you are. Go to that line of code that it's pointing you to, and see if the problem jumps out at you. For example, if it is an immediately apparent syntax error or argument error, it might be very obvious right in that line of code.

But what about when it's not so obvious? We know that sometimes some line of code somewhere else, which is connected to the line of code generating the error, is the cause of our problems.

There are two common errors that one might encounter that cause a method/program not to work.

  1. The first is when it is clear that the program is able to find all of its parts and run successfully, but what you run is returning the wrong thing. In this case, the problem is within your code. Proceed to Step 2.

  2. The second is connectivity (whether the file in question is set up correctly, and linked correctly to its components). You might come across an error like this if you get a missing method or missing class error. In cases like this, proceed to step 3.

Step 2: Break it Apart

When we know what step in your code isn't working, it's best to use a debugger tool such as Pry. Every language has it's own Pry, maybe called something different.

Essentially we use debuggers to examine program states. It stops your code at a given point so we can check two main things - what the variables are, and whether the method calls in a call stack (series of method calls) run. We use it to investigate each part of our code, to locate where the error might be coming from.

Like Jesus, you have essentially descended from the heavens into your code to try to figure out what's going on.

Use Pry to break apart the line of code that you are struggling with, and test each part of it.

For example, given this block of code:

num_followers = 5000;

def JesusMagic(num_of_sth)
    num_of_sth * 1000
end

def MagicMultiply(loaves_of_bread, fish)
    portion_bread = loaves_of_bread / num_followers
    portion_fish = fish / num_followers
    more_bread = JesusMagic(portion_bread)
    more_fish = JesusMagic(portion_fish)
    binding.pry //I'm here guys! Love, Jesus
    puts "Each of Jesus's #{num_followers} followers actually ate #{more_bread} bread and #{more_fish} fish, which he magicked out of #{loaves_of_bread} loaves of bread and #{fish} fish. Go Jesus!"
end

MagicMultiply(5, 3)

Pry will stop the method right after we define more_fish, and before we return our final string. At this point we:

Test the variables.

What is num_followers?
What is loaves_of_bread?

Is it what we think it should be?

Oops! The code above is returning 0 for num_followers. That means it's probably not reading that variable. If we try putting it inside the method, it returns 5000, correctly.


Test the methods.

Given a random number, like 2...

What is JesusMagic(2)?
Is it what we think it should be?

What is portion_bread at this stop point? Is it indeed 5 divided by 5000? Check it on a calculator.

What is portion_fish at this stop point? Is it indeed 3 divided by 5000?
Check it on a calculator.

Hot damn! Testing portion_bread and portion_fish we find that the operation returns 0, and after some googling realize that when we are expecting a number with a decimal to be returned, the number that's being divided needs to be a float (end with .0).


Test the variables and method in combination.

What is more_bread? Does it multiply portion_bread by 100?

What is more_fish? Does it multiply portion_fish by 100?

This seems fine.


Using pry, you can also google methods you haven't tried before and try using them as well in an attempt to return the right thing.

What if we typed in JesusMagic(loaves_of_bread / num_followers)? Does that still work? Would that be a better alternative?

Progressively test in bigger and bigger chunks, until you're testing for the whole method/call.

Where you're finding that something is equal to something it's not supposed to be, or generating another error, you're getting closer to the root of the problem.


So in the example above, factoring in our corrections we get:

def JesusMagic(num_of_sth)
    num_of_sth * 1000
end

def MagicMultiply(loaves_of_bread, fish)
	num_followers = 5000
    portion_bread = loaves_of_bread.to_f / num_followers
    portion_fish = fish.to_f / num_followers
    more_bread = JesusMagic(portion_bread)
    more_fish = JesusMagic(portion_fish)
    puts "Each of Jesus's #{num_followers} followers actually ate #{more_bread} bread and #{more_fish} fish, which he magicked out of #{loaves_of_bread} loaves of bread and #{fish} fish. Go Jesus!"
end

MagicMultiply(5, 3)

Each of Jesus's 5000 followers actually ate 1.0 bread and 0.6 fish, which he magicked out of 5 loaves of bread and 3 fish. Go Jesus!

Much better.


Step 3: Check your Bases

If the error is coming from missing components, methods, classes, or instances, the first thing to do is to check your bases:

  1. Examine your assumptions: Are you working in the right place? Is the code you're writing meant to be there? Check other files or look online for clues to see if other people are putting that block of code in the same place.
    2.Check your access: Can you access the variables and methods you're trying to use? If variables - Are they inside or outside of the method? Do you have read/write access where you're attempting to call them? If methods - are they defined correctly as class or instance? Are you putting in the right amount or format of arguments?
  2. Spelling and Syntax: Are the syntax and spelling of your methods within your method definitions correct? If the error reports that something is missing, are all of your necessary files correctly named and linked?
  3. Environment: Is the order of linked files correct? For example, are you attempting to run methods or classes which are linked in your environment after the one you've invoked it in? Are you missing gems?

If just thinking through these things doesn't yield any answers, you can try the following techniques.

More General Techniques to Locate the Problem

Divide and Conquer

One strategy that might be helpful with this type of error is to divide and conquer. You can comment out different chunks of your code to try to narrow down where the problem is coming from.

This is a strategy also known as the wolf fence algorithm.

Edward Gauss described this simple but very useful and now famous algorithm in a 1982 article for communications of the ACM as follows: "There's one wolf in Alaska; how do you find it? First build a fence down the middle of the state, wait for the wolf to howl, determine which side of the fence it is on. Repeat process on that side only, until you get to the point where you can see the wolf."

This can be particularly useful for situations when you aren't sure which part of your code is causing something to be missing, or which call is causing the problem. This can be an effective alternative to firing up your debugger, particularly if you're not sure yet what exactly to debug.

Print Debugging

Another technique that can be used is to make full use of your console, and have your code report back to you at every point something has been executed. This means to use 'puts' or 'console.log()' or whatever equivalent in your language will put out a message in your terminal as a little flag for yourself to say, "This step has been done."

In the example above:

def saintedNames
disciples = ["Philip", "Andrew", "Thomas", "John", "James", "Judas", "Matthew", "Simon", "Simon", "Thaddeus", "Bartholomew", "James"]
    judas = disciples.find{|disciple| disciple == "Judas"}
    puts "we found #{judas}"
    disciples.delete(judas)
    puts "judas was deleted, now there are #{disciples.count} disciples"
    disciples.uniq
    puts "eliminated all repeated names, now disciples = #{disciples}"
    disciples.map {|disciple| "St. #{disciple}"}
end

saintedNames

What you should get is a series of flags indicating our flow of execution:
"we found 'Judas'" <- Flag that this step was completed successfully
"judas was deleted, now there are 11 disciples" <- Flag that this step was completed successfully.
"eliminated all repeated names, now disciples = ["Philip", "Andrew", "Thomas", "John", "James", "Matthew", "Simon", "Thaddeus", "Bartholomew"]" <- Flag that this step was completed successfully.
["St. Philip", "St. Andrew", "St. Thomas", "St. John", "St. James", "St. Matthew", "St. Simon", "St. Thaddeus", "St. Bartholomew"] <- Your successful code.

Step 4 - Read the test specs, and use Pry to simulate scenarios

One thing that will always help your code is to test it on a variety of example to see what it does, and you can do this within Pry. It gives you a sort of environment you can play around in, to see what you can get. And it's much faster to test things out in Pry than to run your test suite over and over again. You can even test examples in your test suite in Pry!

For example, given our test:

describe 'Angel' do
  before do
    `@michael = Angel.new(name: "Michael")
  end
 it 'has a name' do
    expect(Angel.find_by(name: "Michael")).to eq(@michael)
  end
end

And our code:

class Angel
    @@all = []
    attr_accessor :name
    def initialize(name:name)
        @name = name
    end
    def self.all
        @@all
    end
    def self.find_by(name)
        Angel.all.find{|angel| angel.name = name}
    end
 end
 binding.pry

Within binding.pry we can directly run:
Angel.new(name: "Michael")
Angel.find_by("Michael")
And helpful errors will appear directly within the console, if there are any. We can then adjust our code accordingly. Passing this will ensure that we pass our test. So much faster than running the entire suite of tests every time we try something new!

You can also use this space to challenge your code and see the limits of what's possible. For example, if you run Angel.new(name: "") will it pass? If you run Angel.new(name: 0) Should you try to prevent against these cases?


This article is by no means comprehensive in terms of what you can do to debug but I hope it is helpful.

Links

Ruby Guide to Debugging
Specifically dealing with Ruby error messages, how to read them, and other tools Ruby provides to debug.

Techniques for Debugging
General and concise recap of different debugging techniques.

Debugging Javascript in Google Chrome - Gordon Zhu
This is one of the best videos I've ever seen that teaches about debugging with a tool like Pry, but for Javascript. It specifically focuses on using it to debug iterations - where by putting the debugger in the middle of your method definition, you can literally check the changes to your variables as of every iteration call. For example, in a 'for each' situation you can see how variables change every time the next item is called.


via GIPHY