DEV Community

Cover image for A beginners guide to TDD, JavaScript
venture
venture

Posted on

A beginners guide to TDD, JavaScript

Disclaimer: This is a tutorial and an introduction to TDD, not an in-depth and dogmatic approach to real-life projects. We suggest that you practice this skill before deciding to change the way you code.

What is TDD? In brief: TDD stands for Test-Driven Development. This means that you will develop your code by creating tests first. Those tests will tell you what your code should do. First you will write a test — it will fail because your code can not do what the test demands — and after that you will write the code to make it pass. The repetition of “test” to “code” is a cycle. These cycles repeat over and over until your application is complete. After each one of the cycles we will do a refactor to improve the code quality and reorder things a little. Refactoring is a common practice that every experienced software developer should master.

“Refactoring is the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure.” — Martin Fowler

The cycle, test fail, test pass, refactor

The tests that we will write for TDD are unit tests.

UNIT TESTING is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software. It usually has one or a few inputs and usually a single output. In procedural programming, a unit may be an individual program, function, procedure, etc. In object-oriented programming, the smallest unit is a method, which may belong to a base/ super class, abstract class or derived/ child class. See More about unit testing here

Let's learn by practice with an example. Imagine you want to build a calculator with a set of methods: add, subtract, multiply and you want to do it with TDD.

First we will need our Calculator class (if you decide to program using classes, if you are using JavaScript you could use modules or any other kind of abstraction).

class Calculator {
  // This is an empty class
}
Enter fullscreen mode Exit fullscreen mode

For this class we will create our first test:

describe("Calculator", function() {
  var calculator;
  beforeEach(function() {
    calculator = new Calculator();
  });
  it("should have an add method", function() {
    expect(calculator.add).toBeDefined()
  });
});
Enter fullscreen mode Exit fullscreen mode

This code we just saw is a Test. This test is saying that the calculator should have an add method. If we run this test, it will fail, showing something like this:

If we want to do TDD the next step would be to make the test go green. We will do so by implementing the add method

class Calculator {
  add = () => {
    // Empty method
  }
}
Enter fullscreen mode Exit fullscreen mode

After we created the method we can see this test run green:

This is an example of a cycle of testing and implementation. Ideally we should not test that a method exists, we should test what a method does.

So… now that we understand how the cycles work, let’s go a bit further on the implementation:

Test the add method

describe('add method', () => {
  it('should return 4 when receiving 2 and 2', () => {
    expect(calculator.add(2, 2)).toEqual(4)
  })
})
Enter fullscreen mode Exit fullscreen mode

After we execute the test we see this screen:

Then let’s implement the add method.

class Calculator {
  add = () => {
    return 4
  }
}
Enter fullscreen mode Exit fullscreen mode

After implementing it we launch our tests again and… green!

Wait. What?

Yeah. We passed the test. But the logic of our code… is just wrong! A calculator should not work like that.

If we focus only on passing our tests we can create undesired behaviors. This calculator we coded is passing all of our unit tests but it does not work as a real calculator.

As a software developer you should always try to understand the real needs your software is going to accomplish. Just trying to do what you are told is never the way to become a real professional.

Understanding problems in-depth and giving the correct solution, trying to be pragmatic, and avoiding dogmatism are ways to be a better software professional.

Some developers say that TDD should only focus on solving what the test says. And if the software does not accomplish the real goals is because the test is not enough. I disagree. I say we should understand the real problem. People who write tests (usually developers) tend to forget about some edge cases to test or even miss some required functionalities that were poorly communicated. If we focus only on solving what the test demands we will lack in quality.

I am not talking about programming in a defensive way, trying to solve all the possible ideas we have in our mind, but to deliver the right value to our clients. To understand.

Now, moving back to our test, let's see an example of a more complete test scenario:

describe('add method', () => {
  it('should return a SUM when receiving two different numbers', () => {
    for (var i = 0; i < 100; i++) {
      const valueA = Math.round(Math.random() * 100)
      const valueB = Math.round(Math.random() * 100)
      const sum = valueA + valueB
      expect(calculator.add(valueA, valueB)).toEqual(sum)
    }
  })
})
Enter fullscreen mode Exit fullscreen mode

What we are doing is generating 100 random value pairs and seeing if the calculator can give the expected result. We could also work with a defined preset of values that we want to test. There are different things we could test: negative numbers -1, decimal numbers 0.4, really big numbers 10000000000000000000...00 or even unexpected values like strings, arrays, undefined

In this case we will continue by not trying to be smarter than we need. We will implement our add method and move on.

class Calculator {
  add = (a, b) => {
    return a + b
  }
}
Enter fullscreen mode Exit fullscreen mode

We could do the same with the different methods for our calculator. First we implement the test and later on we implement the functionality. Just remember not to be too clever and also not too simple. Just doing what the test demands could be enough in some cases, in others may be nothing of value at all.

In our projects we try to do TDD when working on new components, API methods, or isolated pieces of code that can be unit tested.

We don’t use TDD as a way to deliver all of our software, — sometimes other tools and techniques come more in hand for other scenarios — but we like to practice with it to become better software professionals.

If you are learning your way to the mastery you can try other XP practices:

  • Continuous Integration and deployment
  • Pair programing
  • Small feedback loops

Want to read more about TDD?

Top comments (1)

Collapse
 
thisdotmedia_staff profile image
This Dot Media

Really helpful article for beginners Rafael! Thanks for sharing 😊