Prep

Playing computer

Learning Objectives

Let’s take this code:

1
2
3
4
5
6
7
const decimalNumber = 0.5;

function convertToPercentage() {
  const percentage = `${decimalNumber * 100}%`;
}

convertToPercentage(0.5);

To understand how convertToPercentage works we must build a mental model of how the computer executes our code. To build this model, we use a method called playing computer🧶🧶 playing computerPlaying computer means simulating how the computer executes our code. We “step through” the code, line by line, and work out what the computer does when it follows each instruction.

To introduce you to the idea, we will use an interactive code visualiser to play computer.

🕹️👣 Step through

In a JavaScript program, each line is an instruction that will have some effect. For example, a line of code with a variable declaration means “store a new variable with this value in memory”. In the interactive widget, arrows are used to show which line just executed and which line is next to be executed.

Click next to see what happens when the computer executes the following program. Pay particular attention to what happens when the function convertToPercentage is called.

Global frame

As we step through the program, we keep track of two things: memory and the line that is being currently executed. We keep track of this information using a frame🧶🧶 frameThink of a frame as the context in which some code gets executed. We use frames to keep track of memory and the line of code that is being currently executed. .

The global frame is always the first frame that gets created when our program starts executing. It is like the starting point for our program, the place where code gets executed first. When we run the code above, decimalNumber and convertToPercentage are both stored in the global frame.

Local frame

💡recall

A function call is an instruction to run the code inside a function

Whenever we call a function a new frame is created for executing the code inside that function. In the example above, we call the function convertToPercentage on line 7 and then a new frame is created for convertToPercentage. Inside the convertToPercentage frame, the computer executes the instructions inside convertToPercentage, storing new variables in memory and keeping track of the current line that is being executed.

Package Management in JavaScript

Learning Objectives

In the last module we started to write reusable blocks of code by defining functions. Using functions helps to keep our code clean and maintainable, and as an added bonus we only need to write the logic out once! We’re not the only developers doing this though - everyone is trying to reuse code wherever they can.

This practice is an established part of a typical workflow and every language has its own tools to support this. In this section we will look at how we can set up one of the JavaScript tools to support our development.

Setting up a package manager.

When we bundle code together and share it we publish it as a package. In order to use someone else’s code in our projects we need to use a package manager to install it. The package manager we will use is called npm.

💡Other package managers

npm is not the only package manager available for JavaScript, yarn is a popular alternative. If you have experience in other languages you may have used package managers there, eg. Python users may have used pip. Each tool has its own commands and ecosystem but the core concepts are the same.

Before we start using npm in a project we need to so some setup. It was already installed for us when we set up Node but we also need to configure the project. Create a new directory called packages-practice and navigate there in your terminal. Once you are there use the command npm init -y to start the setup.

username/cyf-work/packages-practice
npm init -y

You should see some output printed:

Wrote to username/cyf-work/packages-practice/package.json:

{
  "name": "packages-practice",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "type": "commonjs"
}

Look carefully at the first line: it says something was written to a file. If we check using ls we’ll see that there’s now a file called package.json, and if we open the directory in VSCode we see that it includes all the information printed above. Now we have this file we can use npm to do a few different things with our project, but for now we’ll focus on using other packages from our package.

📖Definition: JSON

This file is written in JSON - JavaScript Object Notation. Values before a colon are keys and the values after the colons are the associated values. Using this structure we can quickly find important information about our project.

We will look at JavaScript objects in more detail in the next module.

💡The `-y` flag

In this example we added -y to the end of the setup command. This was optional, but by including it we prep-populated package.json with some common default values. If you don’t include the flag the command will still work but you will be prompted to add a value for each property before the file is created.

Installing a dependency

We’re going to add our first dependency🧶🧶 DependencyA dependency is code which we want to be able to use from our code. . We’re going to use is-odd which provides logic to check if a number is odd or not. This is a very simple example of the workflow, but the process would be the same if we were adding a more complex package.

💡npmjs.com

The link above leads to www.npmjs.com. This site has a searchable list of packages available to install through npm - if you’re looking for something specific you should start here!

Switch back to your terminal and make sure you are in the same directory as package.json, then type the command below:

npm install --save is-odd

Let’s break down the command:

  • npm indicates that the command is run using npm
  • install indicates that we want to install a package
  • the --save flag adds additional instructions about how we save the package. We will see some alternatives later in this sprint.
  • is-odd is the name of the package we want to install

Switch back to VSCode and you will see some new information at the bottom of package.json:

package.json
{
  // ...
  "dependencies": {
    "is-odd": "^3.0.1"
  }
}

The is-odd package is now listed in our project as a dependency. This is important information for anyone else who wants to run our project: it tells them that our code depends on something from is-odd and they will need to install it too.

Take a look in the file explorer tab and you will see there is also now a folder called node_modules. If you open it up you will see a directory for our is-odd package which contains the code it needs to run. When we ran npm install this is what was downloaded. There is also a directory for something else called is-number, which is a dependency of is-odd. It’s very common for additional packages to be installed to support the one we need.

💡Tip

Think: How did npm know that it needed to install is-number?

It downloaded is-odd, read its package.json file, and saw that is-number was listed in its dependencies!

Think back to the last sprint where we spoke about .gitignore files. In that section we saw a .gitignore with node_modules already included in it, and now we can start to see why. If we tried to track everything in node_modules with Git we would end up with a very bloated repository and the potential for lots of conflicts. Instead we ignore the folder and ask anyone using our code to download their own copy of the packages.

Using a Package

Learning Objectives

We have added a package to our project, now it’s time to use it.

importing the package

Before we can use the package we need a file to work in. Create a new file called checkingOddNumbers.js in your packages-practice directory.

We also need to make a change to the package.json file. The way we load the package into our code depends on how the project is configured, so we need to update the type property on line 12. Change its value to module as shown below:

package.json
{
  // ...
  "type": "module",
  // ...
}

Now we can start using the is-odd package. At the top of checkingOddNumbers.js we need to add an import statement. Any time we need to use code which is defined in a different file we need to import it.

checkingOddNumbers.js
import isOdd from 'is-odd';

Generally we will specify which functions we want to import (isOdd in this case) to avoid bloating our program too much. When importing from a package we only need to provide the name of the package in quotes, when importing from another file in our project we need to give its relative path.

Using the package

Once we have imported the code we can use it just like any other function we defined ourselves. Try it by calling it a couple of times and printing the values.

checkingOddNumbers.js
import isOdd from 'is-odd';

console.log(isOdd(1));
// true

console.log(isOdd(2));
// false

In the rest of this sprint we will be following a similar workflow: add a package using npm; import it into our files; use the functions it provides.

✍️Exercise: Using a package

Try to recreate the workflow for yourself.

  1. Create a new directory called translating-five
  2. Initialise an npm project there - you can use the default values
  3. Install the five package. It does fun things with the number 5
  4. Create a new file to work in and import the package
  5. Use the documentation on npmjs to help you translate “five” into the following languages. You should print Five in <language> is <answer>:
    • Dutch
    • Japanese
    • Binary

Using a Testing Library

Learning Objectives

Last sprint we wrote our first unit tests using the assertion libraries built in to Node. They did the job for us, but they can’t do everything. There will be times when we need to bring in specialised tools to help.

In this section we will look at how we can test our code using a testing framework. We’re going to use Jest, which is one of the most popular JavaScript testing frameworks. We can find out more about Jest from the documentation. We’re going to recreate the tests we wrote last sprint using Jest and see how it compare to using node:test.

Installing Jest

Before we can start using Jest we need a fresh directory to work in.

  • Create a new directory called testing-with-jest. Make sure you are outside the packages-practice directory.
  • Copy the timeConverter.js file from the last sprint into this directory.
  • Create a new file called timeConverter.test.js
  • Import formatAs12HourClock() into the test file

We’re going to install Jest using npm. First we need to use npm init -y to create package.json like before, then we install Jest. There’s going to be a slight difference this time though:

username/cyf-work/time-conversion
npm init -y
npm install --save-dev jest

⚠️Warning

Make sure to also set the type key to the value "module" - we always want to do this when making a new package.

This time we have included the --save-dev flag with the install command. Let’s see what that changed in package.json:

package.json
{
  // ...
  "devDependencies": {
    "jest": "^30.5.0"
  }
}

This time we have a devDependencies key instead of dependencies. There won’t be a difference in terms of how we use the packages while we are writing code, but the two are handled differently when the time comes to deploy our code. Certain dependencies support core parts of our program, such as checking if a number is odd in the previous example. Others are only useful while we are still developing. Testing falls into the second category: our end users won’t need to run the tests when they have the finished app in front of them. Those dependencies are marked as devDependencies.

Version numbers

Every dependency we install has an associated version number. In this example we have installed version 30.5.0 of Jest. If a new version of a package is released these digits will change and npmjs has an article explaining what each digit represents. It’s important to keep a record of which version of a package we have used in development.

That applies to our packages’ dependencies too, which is where the package-lock.json file comes in. This keeps track of the version numbers of every dependency in our tree so we can exactly recreate the structure of our program later, even if something in the middle of the tree receives an update.

Testing with Jest

Learning Objectives

Let’s revisit formatAs12HourClock() and test it using Jest.

Defining a test

We’re going to use Jest’s test() function to define our test. Jest is a little different from other packages in that we don’t need to import the functions to be able to use them.

Every time we use test() we need to pass it two arguments:

  • A string describing what we’re testing
  • A function where we will call the function we are testing and define the expected outcome

We’ll start by providing the string and an empty function.

timeConverter.test.js
import {formatAs12HourClock} from "./timeConverter";

test("correctly convert time after 12:00", () => {
  // TODO
});

Inside the function we are going to use two more functions from Jest:

  • expect() will be used to call the function we are testing and capture the actual value returned
  • toEqual() will be used to provide the expected value
timeConverter.test.js
import {formatAs12HourClock} from "./timeConverter";

test("correctly convert time after 12:00", () => {
  expect(formatAs12HourClock("23:00")).toEqual("11:00 pm");
});

When we run our test:

  1. The value "23:00" will be passed to formatAs12HourClock
  2. The code in the function will be executed and the returned value will be stored as the actual value by expect()
  3. The actual value will be compared to the expected value passed to toEqual()
  4. The test will pass if the two values match. If they don’t it will fail.

Running the test

If we try to run timeConverter.test.js using Node we’ll get an error. That’s because Jest isn’t designed to be run in the same way as a typical program, we’ll need to use npm to help us out.

Take a look at package.json and you’ll see a scripts property with a nested object as its value. We can define scripts which can execute larger processes when we type npm run {scriptName}. We already have a value defined for test:

package.json
{
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  }
}

Try running it by typing npm run test in the terminal and see what happens:

npm run test

Error: no test specified

This is telling us that we haven’t told npm how to run our tests. There may be different ways (e.g. if we were using node:test we should run node timeConverter.test.js). We need to tell npm how it should run our tests. Replace the string associated with test with the one shown below:

package.json
{
  "scripts": {
    "test": "node --experimental-vm-modules ./node_modules/.bin/jest"
  }
}

Now try running npm run test again. This time the test should run successfully and log the results to the terminal. You should see the string you passed to test() copied there with a check mark beside it to indicate that the test passed. Success!

✍️Exercise: More than just equality

The toEqual() function is an example of a matcher. Using the Jest documentation read about some other matchers which are available and identify which one would be most appropriate to use in each of these tests:

  1. Checking if a function returns a value above a given minimum
  2. Adding two decimal numbers
  3. A function’s return value isn’t null
Solutions:
  1. toBeGreaterThan()
  2. toBeCloseTo()
  3. not.toBeNull()

Test-Driven Development

Learning Objectives

So far we have been writing our tests after we have written our functions and using them to confirm that the functions do what they are supposed to do. The tests we write are still valid, but by taking this approach we risk confirmation bias - we write the tests to prove something we already know to be true.

We can avoid this by writing the tests before we write any code. This process is called test-driven development (TDD). We write tests which cover the desired behaviour of our function, then write the code to make those tests pass.

How could TDD have helped last sprint?

Think back to the last sprint and how we built up the test suite for convertTo12HourClock:

  • We wrote a test to ensure it worked for an afternoon time ("23:00")
  • We wrote a test to ensure it worked for a morning time and discovered a bug ("08:00")
  • We realised we forgot an edge case ("00:00") and had to modify the function again.

After each step we thought we were done, but we weren’t. We’re still not finished now - we haven’t written any tests to validate inputs, or checked early afternoon times. Because we were working in this file a lot while we learned about testing we found each of these issues quickly, but if we were working on a real-world project there could be a long time between “finishing” the code, discovering a missing test and fixing any bugs that arise. That’s a lot of opportunities for something to go wrong.

Instead our workflow could have been:

  • Write tests for afternoon time, morning time and the midnight edge case
  • Write our first attempt at the function body
  • See some tests pass and some fail
  • Immediately fix bugs or add missing logic

We know what we need to do before we even start coding and we have the tools in place to identify problems before we declare ourselves finished. It doesn’t guarantee that our code will be perfect, but it means many of the potential problems will be fixed before we declare ourselves “finished”.

Red-Green-Refactor

An important aspect of TDD is the need to verify that our code is what’s making the test pass. That means ensuring that the test isn’t passing by itself without us writing anything. If that happens we may have a poorly-defined test.

Watching the tests fail first is part of the red-green-refactor cycle:

graph red[Write a failing test] --> green[Write code to pass the test]; green --> Refactor; Refactor --> red; style red fill:#FFC9C9 style green fill:#B3F2BB style Refactor fill:#A5D8FE
  • Write a test
  • Run the test file and watch the test fail
  • Write enough code to make the test pass - no more than necessary!
  • Run the test again and make sure it passes
  • Refactor the code if necessary to improve readability or efficiency
  • Run the test again to make sure it still passes
  • Repeat with the next test

After modifying the function being tested we should always re-run all of the tests, not just those for the feature we are writing. Making a change in one place can easily break something somewhere else.

It can be difficult to get into the TDD mindset, but once we do there are real benefits to it. In the next section we’ll look at an in-depth example of the TDD workflow.

Test-Driven Development in Practice

Learning Objectives

We’ve seen how to write tests, how to interpret the results and how to add testing libraries to our projects. Now it’s time to pull it all together…

The problem

We’re going to look at a common coding problem which is based on a children’s game called fizz buzz. In the game players sit in a circle and take it in turns to count up from the number 1. Certain numbers are replaced by the words “fizz” or “buzz” and if a player says their number instead of one of those words they are out of the game.

The logic for this puzzle is fairly simple and as a result it has become a popular choice to assess candidates in technical interviews. We’re going to use test-driven development to write a function which takes a number as an argument and returns the appropriate value as a string.

There are many variations on the game but we’re going to stick with the standard rules:

  • If a number is divisible by 3 return “fizz”: 3 --> "fizz"
  • If a number is divisible by 5 return “buzz”: 5 --> "buzz"
  • If a number is divisible by 3 and 5 return “fizzbuzz”: 15 --> "fizzbuzz"
  • If a number is divisible by neither 3 nor 5 return the number as a string: 7 --> "7"

For the purposes of this example we will assume that our inputs will all be numbers greater than 0. If we were doing this for real we should be checking that and writing tests to ensure we handle those cases too!

Setting up

Before we start coding we need an environment to work in.

✍️Exercise: Set up the project

Create the necessary directory and files for this project. We’ll need a file to write our function in (let’s call it fizzbuzz.js) and a file for our tests. Don’t forget to initialise a Git repository!

We have already reached our first major decision point: which testing framework should we use?

As a general rule we don’t want to add any more to our projects than we need to. We certainly don’t need to consider things like front end simulation or database integrations here, so we don’t need tools with that level of complexity. All we need to do is compare the output of a function to an expected value. We could use jest, but that would mean configuring npm, adding packages and writing a script to run our tests. It will be much more straightforward, and ultimately more efficient, to use node:test in this case.

The first test

Before writing any test in this exercise, think back to the diagram in the previous section:

graph red[Write a failing test] --> green[Write code to pass the test]; green --> Refactor; Refactor --> red; style red fill:#FFC9C9 style green fill:#B3F2BB style Refactor fill:#A5D8FE

When we write our first test we should watch it fail before starting to work on the function. Let’s start with the first case in our specification: division by 3.

💡The `describe` block

Most testing frameworks will let us organise our tests using a describe block. Their structure is similar to a test: their first argument is a string describing the block’s content and the second is a function. We will use them to group related tests together like this:

fizzbuzz.test.js
describe('division by 3', () => { 

   // Tests are defined here 

});

Our first test will check that fizzbuzz(3) will return "fizz". Set it up as shown below:

fizzbuzz.test.js
import {fizzbuzz} from './fizzbuzz.js';
import assert from 'node:assert';
import {test, describe} from 'node:test';

describe('division by 3', () => { 

    test('3 returns fizz', () => {
        assert.equal(fizzbuzz(3), "fizz");
    });

});

Running the test doesn’t give us a “pass” or “fail” output though, it throws an error.

✍️Exercise: diagnose the error

Let’s put your debugging skills into practice! Read the error message and research what’s causing it.

Solution:

The test can’t find a function called fizzbuzz being exported from fizzbuzz.js. That shouldn’t be a surprise though - we haven’t written it yet!

This step may seem pointless but it’s still an important one to take. We may not learn anything new from watching this test fail, but if it passes at this stage then it tells us that we have made a mistake in setting up the test.

Let’s switch files and fix the problem. We don’t want to go too far here, even though it can be tempting. Remember that when we are following TDD we write the tests such that they check our program does everything laid out in its specification. If something is in the spec there should be a test for it. That means that if our tests pass, our program does what it’s supposed to do. If our tests fail they will tell us why, so we fix the problem. If we do any more than fix the problem in front of us we may end up writing more code than we need to, which may ultimately not have test coverage.

All of that means that we should only write enough code to fix the error:

fizzbuzz.js
function fizzbuzz(){};

export {fizzbuzz};

That’s it! It feels strange stopping there but that’s all the information we had to work with. We did fix the error though, and now we have a different one to guide our next step. We’re getting an assertion error: our expected value is "fizz" but our actual value is undefined. It’s another easy bug to fix, and just like before we’ll do just enough to fix it.

fizzbuzz.js
function fizzbuzz(){
  return "fizz";
};

export {fizzbuzz};

Our test passes, so let’s move on to the next requirement.

💡Using Git with TDD

We have a test and it’s passing, so now would be an excellent time to make a commit! Our commits should represent stable points we can roll back to if necessary, so committing when all our current tests pass means that our code was working at that point in time. If we make a change and something goes wrong we know that everything will be fine if we revert to this commit.

Testing the next requirement

Our next bullet point is about division by five. We’re testing a different behaviour now so we’ll add another describe block to contain the tests.

fizzbuzz.test.js
describe('division by 5', () => { 

    test('5 returns buzz', () => {
        assert.equal(fizzbuzz(5), "buzz");
    });

});

We have another assertion error, this time we expect "buzz" but our actual value is "fizz". That shouldn’t be a surprise - the only thing our function does is return "fizz"!

✍️Exercise: fix the error

Update the fizzbuzz function so that the test passes. Remember that we only need to write enough code to make the test pass!

Solution:

The only thing we know for sure from our tests is that when fizzbuzz() receives 5 as an argument it should return "buzz", so that’s the only thing we will check for in the function. We don’t have test coverage for anything else.

We also uncover another problem: we haven’t defined a parameter for the function! This wasn’t a problem before because we were returning "fizz" for everything, but now we need to check the value passed to the function. This goes to show that even if we think we have good test coverage we can still miss some fairly major issues.

fizzbuzz.js
function fizzbuzz(number){
  if (number === 5){
    return "buzz";
  }
  return "fizz";
};

Note that we still run all of our tests. We need to be sure we haven’t introduced a bug anywhere else when making changes.

We know this implementation isn’t correct, though. The test cases we’ve written are examples, but they’re only testing one example. As we saw in this implementation, we can just hard-code results for individual values, but this isn’t a general solution!

So let’s write tests for a few more examples, to make sure we’re not hard-coding answers:

fizzbuzz.test.js
describe('division by 3', () => { 

    test('3 returns fizz', () => {
        assert.equal(fizzbuzz(3), "fizz");
    });

});

describe('division by 5', () => { 

    test('5 returns buzz', () => {
        assert.equal(fizzbuzz(5), "buzz");
    });

    test('10 returns buzz', () => {
        assert.equal(fizzbuzz(10), "buzz");
    });

    test('95 returns buzz', () => {
        assert.equal(fizzbuzz(95), "buzz");
    });

});

We have failing tests again, with the same assertion error as before.

We could add else-if clauses for each additional number we test but that wouldn’t scale well at all. Instead we need to make our check more general to account for any number which is divisible by five.

✍️Exercise: Make the check generic

Research how to check if one number is divisible by another and update the if statement to return "buzz" for any value divisible by five.

Solution:
fizzbuzz.js
function fizzbuzz(number) {
  if (number % 5 === 0){
    return "buzz";
  }
  return "fizz";
};

✍️Exercise: Testing the next requirement

Create another describe block and write tests for the "fizzbuzz" output. Use 15, 30 and 90 as the inputs.

Solution:
fizzbuzz.test.js
describe('division by 3 and 5', () => { 

    test('15 returns fizzbuzz', () => {
        assert.equal(fizzbuzz(15), "fizzbuzz");
    });

    test('30 returns fizzbuzz', () => {
        assert.equal(fizzbuzz(30), "fizzbuzz");
    });

    test('90 returns fizzbuzz', () => {
        assert.equal(fizzbuzz(90), "fizzbuzz");
    });

});

Assertion errors again, this time expecting "fizzbuzz" and receiving "buzz". No problem though, we’ve done this before. Let’s add another clause to our if statement to handle this case.

fizzbuzz.js
function fizzbuzz(number){
  if (number % 5 === 0){
    return "buzz";
  } else if (number % 15 === 0){
    return "fizzbuzz";
  }
  return "fizz";
};

Running our tests gives us a surprising result though: we have the same three failures with the same three assertion errors. It looks like the fizzbuzz() function is sending all of these inputs down the wrong branch of the if statement.

At this point it would be useful to use VSCode’s debugging tools to step through the code as the test runs and watch how each line is evaluated. That can be an incredibly useful tool when dealing with complex logic and complex function calls but in this situation we can already see what’s going wrong. The question we have is why. Remember that you aren’t limited to the tools in front of you when debugging, and in this case a bit of old-fashioned Googling will probably get us to an answer quicker than the debugger.

The issue is a mathematical one: 15 is divisible by 5, so if a number is divisible by 15 then it is also divisible by 5. Remember that an if statement stops once a condition is satisfied, so by checking division by 5 first we are also catching values which are divisible by 15 and sending them down the wrong path.

✍️Exercise: Re-order the clauses

Update the if statement so that we check for divisibility by 15 before divisibility by 5.

Solution:
fizzbuzz.js
function fizzbuzz(number){
  if (number % 15 === 0){
    return "fizzbuzz";
  } else if (number % 5 === 0){
    return "buzz";
  }
  return "fizz";
};

Now our tests pass! We’re very nearly there, we just have one more requirement to cover: returning the number as a string if not divisible by three or five. Let’s write some tests:

fizzbuzz.test.js
describe('returning the number as a string', () => { 

    test('1 returns "1"', () => {
        assert.equal(fizzbuzz(1), "1");
    });

    test('4 returns "4"', () => {
        assert.equal(fizzbuzz(4), "4");
    });

    test('91 returns "91"', () => {
        assert.equal(fizzbuzz(91), "91");
    });

});

We have three failing tests as expected and all three are failing with similar assertion errors: in each case the actual value is "fizz". At the moment we’re using this as a catch-all value if a number isn’t divisible by fifteen or five, but really it should only be returned if the number is divisible by three. We need to update our logic again.

We could easily pass these new tests by changing the final return statement:

fizzbuzz.js
function fizzbuzz(number){
  if (number % 15 === 0){
    return "fizzbuzz";
  } else if (number % 5 === 0){
    return "buzz";
  }
  return number.toString();
};

This highlights the importance of running all our tests though, as our “division by three” tests are now failing. We fixed one problem but broke something else. We’re going to need to add another clause to the if statement to get all our tests passing:

fizzbuzz.js
function fizzbuzz(number){
  if (number % 15 === 0){
    return "fizzbuzz";
  } else if (number % 5 === 0){
    return "buzz";
  } else if (number % 3 === 0){
    return "fizz";
  }
  return number.toString();
};

Refactoring for quality

Refactoring is an important part of the development lifecycle. Solving a problem is one thing, but solving it well often needs us to make changes for efficiency. We need to consider our future selves too - we need to be able to understand what we wrote!

At the moment we have a solution which works, but could it be better?

We can start by looking at the conditions we are checking. The first clause may be technically correct, but our specification didn’t say anything about checking for division by 15. Instead it spoke about division by 3 and by 5. Mathematically speaking it may be the same thing, but we can certainly make it clearer that this clause relates to that requirement.

fizzbuzz.js
function fizzbuzz(number){
  if (number % 3 === 0 && number % 5 === 0){
    return "fizzbuzz";
  } else if (number % 5 === 0){
    return "buzz";
  } else if (number % 3 === 0){
    return "fizz";
  }
  return number.toString();
};

We should look for refactorings every time all of our tests are passing. The first couple of tests we wrote didn’t have any refactorings, because the code was so simple, but we still want to look out for opportunities to make our code better whenever it works.

Summary

This may feel like a lot of work for a small problem but it has already demonstrated some of the potential issues we can run into. Imagine, for example, that we hadn’t written tests for the "fizzbuzz" cases and just assumed our code was correct. We may not have caught the bug until an actual user was interacting with it and by that point there are many more layers of infrastructure clouding the picture.

By writing the tests first we can make sure that our program’s requirements are captured and represented in a way that gives developers clear feedback if there is a problem with the code. Testing like this is an important skill and it is one we will reinforce throughout the rest of this course.