Lesson Tuesday

In the last lesson, we covered the basics of test-driven development and pseudocoding a test. In this lesson, we'll see how we can apply the principles of TDD to solve a more challenging problem. We aren't taking advantage of all the benefits of TDD yet (and we won't do so until we start using automated testing). For now, we are focused on using TDD to break larger problems into smaller units that are easier to reason about.

Imagine that a person born on February 29th has hired us to build an application. She would like to determine if any given year is a leap year (meaning it's a birthday year for her). Here's a finished example of what she'd like: Leap year detector. In this lesson, we will use TDD to write the business logic for this application. We will not worry about UI logic right now - TDD is not for writing UI logic, only business logic.

So what exactly should our leap year business logic achieve? Let's break this down into the smallest elements possible. In this way, we'll also make our lives easier as coders and we won't be overwhelmed by the scope of our project.

Our business logic needs to take a year as an input and return a boolean. If the inputted year is a leap year, it should return true. If not, it should return false. Naturally, we will want to write a function to determine this for us. We could even write multiple functions, but this problem is small enough that one function will be enough. As usual, our function should be clearly named. It's common convention to prefix the name of a function that returns a boolean with "is" or "has" - this signals to other developers that the function returns a boolean. For that reason, we'll name the function isLeapYear().

Let's think of all of the possibilities we might get from a user and what the correct response should be for each possibility.

Specifications

Timeanddate.com lists three criteria to determine if a year is a leap year:

  • The year is evenly divisible by 4;
  • If the year can be evenly divided by 100, it is NOT a leap year, unless;
  • The year is also evenly divisible by 400. Then it is a leap year.

Each time a user inputs a year to evaluate, we will need to test the value against each of the leap year rules. Here are the things we'll need to test in our business logic.

Leap Year Input-Output Grid

Now that we've broken our code down into manageable behaviors, we can write a spec for each of them. Important note: You are not required to add a table like the one in the chart above to your README, either for this section's classwork or the independent project. You are only required to add specs like the ones we'll be demonstrating in just a moment. However, it can be very helpful to add a table to help plan and visualize the steps necessary in solving a problem. For that reason, adding a table like the one above can be helpful for working through a solution, especially at first.

Test #1

The first spec should return false if a year is not a leap year. How can we turn this into a test?

Describe: isLeapYear()
Test: "It returns false for years that are not a leap year"
Expect(isLeapYear(1993)).toEqual(false);

Note that in this example we didn't have to add a code section. That's because we can directly test whether leapYear(1993) equals false. If you don't need a code section, it's fine to omit it.

The other lines are explanatory: we describe the name of the function and our test line states what the spec is testing in plain English.

Next, we want to write the least amount of code we need to get this test to pass. Here it is:

function isLeapYear(year) {
  return false;
}

We can verify that this works by testing the function in the console or Node REPL:

function isLeapYear(year) {
  return false;
}
isLeapYear(1993);

This will return false - which is exactly what we want from our expectation: Expect(leapYear(1993)).toEqual(false);.

At this point, you may be thinking we've written too little code - after all, our function will currently return false for everything. However, that's okay. Our future tests - along with the code to make them pass - will solve this problem. According to Lao Tzu, a journey of a thousand miles begins with the first step. We aren't traveling quite that far but the sentiment still applies. We are starting with one little step. This is how TDD works - and developing good practices now will make it easier to tackle tougher problems in the future.

Note that this would be a good time to commit code if we were putting this project in a repository. It's customary to write a new commit after each passing test. You will be expected to do so for this section's independent project.

Now it's time to take the next step. Let's address our second behavior.

Test #2

Let's add a second test to the first one:

Describe: isLeapYear()
Test: "It returns false for years that are not a leap year"
Expect(isLeapYear(1993)).toEqual(false);

Test: "It returns true for years that are divisible by 4"
Expect(isLeapYear(2004)).toEqual(true);

We're showing both tests here to highlight the fact that we don't need to add a line for "Describe" for the second test. That's because both tests are part of the same group of tests since they are both related to the isLeapYear() function.

Now let's take a closer look at our second test:

Test: "It returns true for years that are divisible by 4"
Expect(isLeapYear(2004)).toEqual(true);

Once again, we don't need a "Code" section between "Test" and "Expect" - that's because we don't need any additional code to evaluate our expectation. Our "Test" section clearly states that the test should return true for years that are divisible by 4. Our expectation states that isLeapYear(2004) should be true.

Now let's update our function to get our second test passing:

function isLeapYear(year) {
  if (year % 4 === 0) {
    return true;
  } else {
    return false;
  }
}

We've added a conditional here. We use a modulo (%) to check the remainder of year when it's divided by 4. If the remainder is 0 (which means that year is divisible by 4), we want our function to return true.

We can manually test this in the console and verify that isLeapYear(2004) returns true. Note that when we use Jest for automated testing in Intermediate JavaScript, we'll run all of our tests each time we check new functionality in our code. This is really important because sometimes the code that gets a new test to pass will make a previous test fail. It's a bit labor intensive to run all our tests manually so you aren't expected to do that now. However, keep in mind that adding code can break your previous tests, so if you have any doubt about the code you've added, you can manually check your older tests as well.

If we were adding this code to a repo, we'd commit our code now. Remember, it's a best practice to commit after each passing test.

Test #3

Our third test will consider what occurs when a year is divisible by 100. In this case, it's not a leap year. Here's our test:

Test: "It returns false for years that are divisible by 100"
Expect(isLeapYear(2100)).toEqual(false);

It looks very similar to the last test. Once again, we don't need a "Describe" or "Code" section.

Here's the smallest amount of code we need to get this test passing:

function isLeapYear(year) {
  if (year % 100 === 0) {
    return false;
  } else if (year % 4 === 0) {
    return true;
  } else {
    return false;
  }
}

We can verify that the test passes in the console. And while we aren't adding this code to a repository at the moment, if we were, we'd commit our code now. We will keep reminding you about this important best practice - because in the future, you'll be expected to commit after each passing test.

Refactoring

As we get our tests passing, we might discover new opportunities to refactor our code. In fact, we can make our function more concise right now. We don't need to have three different conditionals here - we can make do with two:

function isLeapYear(year) {
  if ((year % 4 === 0) && (year % 100 !== 0)) {
    return true;
  } else {
    return false;
  }
}

We've reduced our code by two lines. We should commit after refactoring our code like this.

Test #4

We have one last spec on our current list. Our program should return true for years divisible by 400, since that means they are a leap year. Let's start with the test, which will look very similar to our other tests so far:

Test: "It returns true for years that are divisible by 400"
Expect(isLeapYear(2000)).toEqual(true);

Now we'll add just enough code to make our last test pass:

function isLeapYear(year) {
  if ((year % 4 === 0) && (year % 100 !== 0) || (year % 400 === 0)) {
    return true;
  } else {
    return false;
  }
}

We can test this out in the console and verify that our test passes.

At this point, we've successfully coded all behaviors in our specs and manually run our tests. As always, we should commit after a passing test.

Remember, you need to include all of your specs in the README of your independent project. For instance, the above specs might look like this in a README:

README which includes specs.

You can also enclose code snippets in backticks in Markdown. An inline snippet is written like this in Markdown:

This is code!

As you can see, there's a backtick at the beginning and end of the line.

Meanwhile, code that is one or more full lines should use three backticks before and after the code snippet. Here's an example:

This is a longer snippet of code... which means it should have three backticks before and after the code snippet. `

Make sure that you include specs in all of your READMEs during this section - and remember that doing so is a requirement for this section's independent project. Whether you use backticks or not is up to you - the most important thing is that they use the correct syntax along with the principles of TDD that we've covered in this lesson.

Lesson 23 of 34
Last updated December 1, 2020