Lesson

We have a bug in our application. But what exactly is it? You'll find it if you test the application out in the browser. This GIF demonstrates exactly what is happening.

Application correctly shows the occurrence of a word unless we don't enter anything into the selected-count field

If we enter a text passage into the first field and a word into the second field, everything works correctly. However, if we don't enter anything in the second field, our application states that the selected word count is the same as the total word count. But how is that possible? It should be zero - or better yet, the selected word count shouldn't show at all if it's not entered.

Considering the latter option, couldn't we just hide the selected word count and only show it if a word is entered in the second field?

Well, sure, we could. And at least from a user perspective, there'd no longer be a bug. But that would be like sweeping dust under the rug so no one else will see it. It's still there. And it could eventually morph into a terrifying dust monster from another dimension if it's left there. Okay, that's not technically true, but the point is, we can't just hide bugs. We need to fix them.

This also gives us a perfect opportunity to revisit some of the debugging techniques we covered in the last section. Remember those? We are especially referring to the techniques in Debugging in JavaScript: Using debugger and Breakpoints.

So let's solve this by adding a breakpoint! We'll review the techniques covered in the lesson linked above because they are absolutely essential tools for debugging your code. Consider this a refresher. Whether or not you are feeling good about using breakpoints yet, they are so important that it's worth covering them again.

At this point, we don't know what's causing the error yet. It seems like we've tested our functions pretty thoroughly, so maybe it's coming from the UI. However, it's still possible we missed something.

Start by opening the console if you haven't already. We always want to keep an eye on the console when we are testing our code in the browser.

If we load our page, there aren't any errors, which means there's nothing obvious just yet.

Next, click on the Sources tab. If you dragged index.html into your browser to open the application, your scripts.js code should already be showing. If not, you'll need to manually add the code by clicking Filesystem in the left pane of Sources and then clicking the + icon as the GIF below shows.

Opening a new workspace for debugging.

Once we have our code open, we need to insert a breakpoint. The GIF below demonstrates the whole debugging process using a breakpoint. We'll also explain it in detail after the GIF and you should walk through the process on your own as well.

We insert a breakpoint to pinpoint where the bug is, then test our function in the console.

First, we click on the line of code where we want the code to pause. Since the error is occurring in the line where we'd expect to see numberOfOccurrencesInText() correctly rendered, let's start by verifying that our numberOfOccurrencesInText() function is working correctly. That means we will add the debugger in the line right after const occurrencesOfWord = numberOfOccurrencesInText(word, passage);. This is line 40 in the GIF, though it might be a slightly different line number in your code.

Next, we manually submit the form again. Our code will execute and when it reaches the breakpoint, it will pause. The console will also show us the result of each line of code, which is very helpful. For instance, we can see the value of word from the input is "". We can also see that when we call const occurrencesOfWord = numberOfOccurrencesInText(word, passage);, occurrencesOfWord is equal to 6. That's not at all what we want.

In other words, the call was coming from inside the house all along! It's not the UI - the bug is coming from the function which we've already tested.

In the GIF above, we then move to the console tab and manually test the function ourselves with a different input but still using an empty string as the word input.

Specifically, we do the following:

numberOfOccurrencesInText("", "hi");

The result is 1, which isn't correct. It's often very helpful to try out code within the scope of the breakpoint. Remember, you can just switch over to the console and check the value of any functions or variables that exist in that scope when your code is paused. And if they don't exist and you expected they would, well, you've got a scope problem. While the values in the Sources tab generally give you a lot of information, we still recommend using the Console to test your code regularly within breakpoints.

This particular problem isn't a scope problem, though. We have a bug in our numberOfOccurrencesInText() function. We wrote a test that checks if the text passage is an empty string but we never tested what would happen if the word itself is an empty string.

Even if we had written that test early on, it would've broken later. That's because we introduced a new bug when we refactored our function to use String.prototype.includes(). Check this out:

"hi".includes("");
true

It turns out that when we use String.prototype.includes(), every string includes "". A really tiny bug, right? Well, look how quickly it came back to bite us. It's easy to forget a simple test like this. In fact, we just did.

If we were using Jest and we'd already written a test for this, Jest would automatically catch the failed test for us. As we'll discover in a few sections, automated testing is a very powerful tool. However, it ony works as well as the tests we write, which means we need to work on good test-writing practices now with our plain English tests.

So let's fix this issue. Here are two possible fixes. Which one do you think is better?

  • Update our UI so that a user can't pass in an empty string or otherwise update the UI so the numberOfOccurrencesInText() function won't be called for an empty string.

  • Update our function so it can handle an empty string input for a word.

If you guessed the second option, you are correct!

Remember, our goal right now is to keep our logic separate. Our function should be robust on its own. If we tried to fix the problem by addressing the UI, our function would get more dependent on external logic (in this case, code that checks the user input in the UI) - and that is not good separation. There's certainly nothing wrong with doing both things. For instance, we could also do something in the UI as well. But they are still two completely different things. On the one hand, we want a robust function that always correctly returns the number of occurrences of a word. On the other, we want to notify a user if they don't input a word. Technically, our function could handle both things but it shouldn't. That would mean our code isn't properly separated.

So what's the next step?

No, it's not time to fix the code yet. First, we need to write a test. Always. No matter how small the fix.

Here's the test. (This should be the last test in the Describe block for numberOfOccurrencesInText()).

Test: "If an empty string is passed in as a word, it should return 0."
Code:
const word = "";
const text = "red RED Red!";
wordCounter(word, text);
Expected Output: 0

The fix is simple:

scripts.js
function numberOfOccurrencesInText(word, text) {
  if ((text.trim().length === 0) || (word.trim().length === 0)) {
    return 0;
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  return wordCount;
}

We just need to update the first conditional to account for both parameters (text and word):

scripts.js
...
if ((text.trim().length === 0) || (word.trim().length === 0)) {
  return 0;
}
...

So our application has a functioning UI now and we've also fixed a bug. However, take careful note of the fact that we still kept business and UI logic separate in the process.

Let's say that we wanted to return a message to the user in the UI if they don't enter a word. We aren't actually going to do this - we just want to illustrate two examples where logic isn't properly separated. One of these examples is very obvious while the other is less so.

Here's the obviously bad example of mixing UI and business logic in our function:

// This is bad! Don't put jQuery logic to alter the DOM in your function!

function numberOfOccurrencesInText(word, text) {
  if ((text.trim().length === 0) || (word.trim().length === 0)) {
    $("#total-count").html(0);
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  $("#total-count").html(wordCount);
}

This example should be really obvious. Instead of returning the number of occurrences, the function doesn't return anything at all. It directly alters the DOM - which should be part of our user interface logic. Whenever you see a function altering the DOM in business logic, it's time to refactor. You shouldn't be doing it in the first place. In general, since jQuery is often used for altering the DOM, putting any jQuery in your business logic will be a problem. There are occasional exceptions, but for now, think of it as a good rule of thumb. If it directly alters the DOM in any way, keep it out of your business logic. That's a job for the UI logic. Examples of altering the DOM include jQuery methods like html(), append(), show(), and so on.

Now let's look at a slightly less obvious example.

// Not so good either, but for less obvious reasons.

function numberOfOccurrencesInText(word, text) {
  if ((text.trim().length === 0) || (word.trim().length === 0)) {
    return "You need to enter both a word and a text passage!";
  }
  const wordArray = text.split(" ");
  let wordCount = 0;
  wordArray.forEach(function(element) {
    if (element.toLowerCase().includes(word.toLowerCase())) {
      wordCount++;
    }
  });
  return "There are " + wordCount " total matches!";
}

Here there's no alteration of the DOM. However, our function still has a problem with separation of logic. How so? Well, it's really nice when our function just returns the number of occurrences of a word in a text. It shouldn't care about anything else. That includes providing a message for a user. A message like "You need to enter both a word and a text passage!" really belongs in the user interface because it's a specific message for a user. If you want to return these kinds of messages, put them in the UI logic instead.

Here's a silly example to hammer home the point. What if "xyz".split("") didn't return the array ["x", "y", "z"]) but instead returned the following message:

"Hooray. You've successfully split the string into three elements:  ['x', 'y', 'z']!"

Array.prototype.split() would go from being one of the most useful methods in JavaScript to being absolutely worthless. Don't do the same to your own code!

In the next lesson, we'll give one more example of UI logic versus business logic. This will hopefully really drive home the difference between the two.

Lesson 17 of 34
Last updated October 12, 2021