What is TDD?

TDD stands for Test Driven Development. Let's see how to apply this method for the FizzBuzz exercise.

TDD definition

Basically when you begin to write a functionality, you have some steps before diving into coding:

  • Identify the need
  • Define the need with words and try to put on paper how the functionality should answer the issue
  • Create a file for tests and set up your environment
  • Write the test
  • Run the test and it should fail as the functionality is not yet implemented
  • Implement the functionality until the test passes
  • Refactor the code

Why do we do this way and not the other way?

It is to ensure that all the requirements from the stakeholders are met and be sure that the functionality that we will write answer this. If you do it on the other way, you will write a test that meets what you coded.

REMEMBER, what you implemented is what you think the feature should do. You should always ask the stakeholders to make sure of what they want.

In practice with Mocha and Chai

Context: you want to implement a functionality that is returning:

  • “Fizz” if the number can be divided by 3
  • “Buzz” if the number can be divided by 5
  • “FizzBuzz” if the number can be divided by 3 and by 5
  • the number if not

Let's write the shell of the test:


describe(‘FizzBuzz functionality’){
  it(‘should return Fizz if the number can be divided by 3 ’, () => {});
  it(‘should return Buzz if the number can be divided by 5 ’, () => {});
  it(‘should return FizzBuzz if the number can be divided by 3 and by 5 ’, () => {});
  it(‘should return number if the number can’t be divided by 3 or 5 ’, () => {});
}          
          

We will focus first on the multiple of 5 only for an example. Let’s write that with the terminology of Chai.js: expect:


describe("FizzBuzz functionality", () => {
  it("should return Buzz if the number can be divided by 5", () => {
    expect(fizzbuzz(5)).to.equal("Buzz");
    expect(fizzbuzz(10)).to.equal("Buzz");
    expect(fizzbuzz(20)).to.equal("Buzz");
  });
});

//This returns an error
  
FizzBuzz functionality
  1) should return Buzz if the number can be divided by 5


0 passing (25ms)
1 failing

1) FizzBuzz functionality
      should return Buzz if the number can be divided by 5:
    TypeError: fizzbuzz is not a function
    at Context. (test\fizzbuzz.test.js:7:12)
    at processImmediate (internal/timers.js:456:21)            
          

If you run the test file, this will fail, as we never implemented the functionality.The next step is to write the actual code.


exports.fizzbuzz = (n) => {
  if(n%5 === 0){
    return('Buzz')
  }
  return n;
};            
          

This will work and pass the test. And then we repeat this process until we get all the tests for all the cases:


describe("FizzBuzz functionality", () => {
  it("should return Buzz if the number can be divided by 5", () => {
    expect(fizzbuzz(5)).to.equal("Buzz");
    expect(fizzbuzz(10)).to.equal("Buzz");
    expect(fizzbuzz(20)).to.equal("Buzz");
  });
  it("should return Fizz if the number can be divided by 3", () => {
    expect(fizzbuzz(3)).to.equal("Fizz");
    expect(fizzbuzz(9)).to.equal("Fizz");
    expect(fizzbuzz(18)).to.equal("Fizz");
  });
  it("should return FizzBuzz if the number can be divided by 3 and by 5", () => {
    expect(fizzbuzz(15)).to.equal("FizzBuzz");
    expect(fizzbuzz(30)).to.equal("FizzBuzz");
  });
  it("should return number if the number can’t be divided by 3 or 5", () => {
    expect(fizzbuzz(7)).to.equal(7);
    expect(fizzbuzz(11)).to.equal(11);
    expect(fizzbuzz(17)).to.equal(17);
  });
});            
          

And just as before we adapt the code for it to pass the tests, even if for now, it is not the best solution. We just want a functionality that passes the tests. We got that:


exports.fizzbuzz = (n) => {
  if(n%3 === 0 && n%5 === 0){
    return('FizzBuzz');
  } else if(n%3 === 0){
    return('Fizz');
  } else if(n%5 === 0){
    return('Buzz');
  } else {
    return n;
  }
};            
          

Now it is time to refactor our code. This means find a solution that passes the tests and that is clean, nice and reusable:


exports.fizzbuzz = (n) => {
  let answer = "";
  n%3 === 0 ? answer += "Fizz" : null;
  n%5 === 0 ? answer += "Buzz" : null;  
  return answer.length > 0 ? answer : n;
};            
          

What's next?

Maybe you think, ok now my code is passing the tests. Then, let's clean and discard the test files.

STOP

Once you've got your tests, keep them. When you or another dev is working on it, these tests will be very important. They will tell us if the new bit of code is breaking a feature!

You can also include them in a pipeline, making sure that when you deploy your feature, all the tests are passing.

Finally, the tests are important as they describe the behaviour of our application. This is therefore a source of information for the developers.

Cool features

it.only()

One little trick I learnt was that you can run one test at the time, if you write it.only() at the beginning instead of it(). This can be very useful to debug. But don’t forget to run all the tests after that to see if you didn't break another test.


it.only('should return a status of 200', async () => {
  const res = await chai.request
    .get(`/`);
  expect(res.status).to.equal(200);
});                    
          

xit()

If you think about one test you will need to implement, but for some reasons you can’t right now, you can still write it and instead of it(), write xit().


xit('should return a status of 200', () => {
  //Your test here
});                             
          

before-after and beforeEach-afterEach

If we need to do some steps before or after the set of tests, we can use them inside the describe. This can be be useful for example if you need to connect your app to a database. Then we can do before the test and then run the test.


after(() => {
  mongoose.disconnect(() => {
    mongoose.models = {};
    mongoose.modelSchemas = {};
    mongoose.connection.close();
  });
});

describe('GET information', () => {
  let agent;
  
  beforeEach(async () => {
    agent = chai.request.agent(app);
    const res = await agent
      .get(`/user`);
  });

  describe('New info object structure', () => {
    it('should be an object', async () => {
      const res = await agent
        .get(`/info`);
      expect(res.body).to.be.an('object');
    });
  });
});
          

async/await

In the case we have to do some API calls for example we can use the pair async/await in the test:


it('should return successful status 200', async () => {
  const res = await agent
    .get(`/`);
  expect(res.status).to.equal(200);
});