Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

TDD Example in Software Development (Part 2)

DZone's Guide to

TDD Example in Software Development (Part 2)

Now that we've reveiwed the basics to TDD, let's take a look at the actual implementation of TDD in this second article of the series.

· DevOps Zone ·
Free Resource

Open source vulnerabilities are on the rise. Read here how to tackle them effectively.

Let’s continue with our series of TDD articles. In the first part, we looked at the theory behind TDD and Unit Testing. In this second part, we begin to develop our application, an application of notes where a user can write notes and everything that comes to our mind. Please, leave comments if you are interested in seeing how we develop any specific functionality in our application.

TDD First Cycle

To start developing our application, we could start with the user entity (quite generic and it is used for everything). We will see later if it is necessary to change it to something more concrete.

We always have to have in mind the TDD cycle. The first step is red, so we look for a single test that fails and once we put it in green, we have a functionality for our application (partial or complete).

In this process, we have to “invent” a possible interface for our entity. It will be evolving because new or better things will arise and it is likely that we will have to modify both the test and the production code. We should not think that once a test is written and put in green, that it cannot be modified or eliminated. Let’s start with a test that verifies that a user has been created with a name and surname, for example:


describe("given user", () => {
  describe("with valid data", () => {
    test("is created", (done: any) => {
      const validUser = new User("someId", "Oscar", "Galindo");
      expect(validUser.toString()).toEqual("User(someId,Oscar,Galindo)");
      done();
    });
  });
});

In this first test, we check with a toString that the created user is the one we expect. To make a test that checks all the properties of an entity that can have a short-term cost as the entity grows, we can ask ourselves if it makes sense to test that all the properties of the entity.

Note: The toString  has not been tested (explicitly) since it is a method we use to do debug/test.

Okay, we already have a test in red as TDD First Cycle indicates "red." Now, we have to put that test in green in the simplest way possible and it may sound absurd, but that is what must be done. TDD helps you to focus on one thing, but of course, it depends on the programmer, so we have to keep in mind the processes that exist and the ones that help us.

The first error appears while compiling. It doesn’t know what the “User” is, so when we create it we should assign input parameters to the properties of the class:

export class User {
  constructor(private id: string, private name: string, private lastname: string) {}
}

In the examples that you may find on the internet, the implementation they usually do in the first iteration is to hardcode as much as possible, distort the response to put the test in green. Well, this is not 100% like that.

Most developers who want to learn TDD see this as a waste of time. It may seem absurd to hardcode the answer when we know the implementation (to put the test in green), and while it is simple, it is also accepted within the TDD. There are several ways to put the test in green in the fastest way possible, although in this chapter I will only mention two of them:

  • Fake it: To falsify the answer in order to put the test in green as quickly as possible.
  • Obvious implementation: If we know the implementation, it is easy to develop and there is little risk of not putting the test in green, go ahead with it.
    • If we do not put the test in green, go back to Fake it. The important thing here is to focus on what the test asks us, nothing more.

Great, we’re already in green, now comes the second part of the refactor. This part is special; I have heard something like “TDD helps you make shit code that works.” It is hard to understand (and it is part of the TDD learning) that TDD doesn’t mean doing tests. TDD, as we have already mentioned, is a process that helps you to design and model your code while taking the smallest possible steps with security. It confirms that what we have in our head is correct and gives us a continuous design space. If you skip this last step, then it means that you are not doing TDD, you are doing TF (Test First). The TDD formula (not exact and arguable) is TF + Refactor = TDD

Having said that, what kind of refactor can we do in our first test? In this case, we will not do anything, we do not have any kind of duplication and we do not want to abstract anything. Let me show you a very good article where you may find a checklist to take into account in each step of the cycle and a great book.

We have the TDD first cycle finished. Based on the first test, I wonder if a user can exist with an empty name, and I also wonder if it can have numbers or more than one million characters, but I focus on one and the others are written down in my To-Do list to go over them step by step (I usually have a ToDo.md inside the app where I’m writing down everything I see), obviously they are validations that we must do and we will do.

At first, as always, we create a test that validates that a name cannot be empty. Easy, right?


describe("with empty name", () => {
    test("should throw", (done: any) => {
      const userThrowWithEmptyNameCreator = () => new User("someId", "", "Galindo");
      expect(userThrowWithEmptyNameCreator).toThrow();
      done();
    });
});

We have got a test that fails before an empty name. This time it tells us that it should give an exception but it is not doing it, something that is normal, is not implemented. We will develop this functionality in the simplest way:


export class User {
  constructor(private name: string, private lastname: string) {
    if (name === "") {
      throw new Error("empty name");
    }
  }
}


We return to green! We have advanced, we have new functionality, we have a user entity with certain validation rules implemented, and the good thing is that it gives us some freedom in making certain refactors safely since we have tests on a validated code that will inform us if we break something.

Once we have it green, we have to refactor what we have, but at the level of the production code, we are not going to touch anything. What about the test code? It is also our code, that code also counts and also has to be maintained. It may raise worries to have a production code difficult to maintain/read, but it is worse if that type of code is found in the tests. If you find this type of code in the tests, two things happen:

  • You stop trusting your tests
  • You do not pay attention to them, skipped tests begin to appear, commented or directly eliminated until eventually there are no tests, and we already know what happens next.

What can we refactor in this little test we have? In these two tests we create a user, we can take that creation to a method of keeping something like this:


describe("given user", () => {
  const createUser = (name: string, lastname: string) => new User("someId", name, lastname);
  const createValidUser = () => createUser("Oscar", "Galindo");
  const createUserWithEmptyName = () => createUser("", "Galindo");

  describe("with valid data", () => {
    test("is created", (done: any) => {
      const validUser = new User("someId", "Oscar", "Galindo");
      expect(validUser.toString()).toEqual("User(someId,Oscar,Galindo)");
      done();
    });
  });

  describe("with empty name", () => {
    test("should throw", (done: any) => {
      expect(createUserWithEmptyName).toThrow();
      done();
    });
  });
});

There are many strategies for creating these objects:

  • The one that we have used was to create a method in the entity creation test, but if that entity grows we can have problems (although I think we could investigate if it is a design problem)
  • Object Mother
  • Test Data Builder

Once refactored, we can see that tests keep being green. We have gained a lot of readability and we have facilitated the maintenance task for other developers (and on top of that we help others understand how the application works because the tests become a part of the documentation of a project).

Now let’s do exactly the same with lastname . We start with the test, it will be practically the same as the one with name . We have to continue with the previous refactor for that, we already have a factory to create users, so why not use it?


const createUserWithEmptyLastName = () => createUser("Oscar", "");
describe("with empty lastname", () => {
    test("should throw", (done: any) => {
      expect(createUserWithEmptyLastName).toThrow();
      done();
    });
});

Test is red. Here the developer starts to lose a bit of focus and I repeat one of the 3 laws of TDD, not to write more code than is enough to pass the test, so, let’s look at this code.


export class User {
  constructor(private name: string, private lastname: string) {
    if (name === "") {
      throw new Error("empty name");
    }
    if (lastname === "") {
      throw new Error("empty lastname");
    }
  }
}

We have the test in green and we have new functionality! Cool, now comes something fun: refactoring, and this time in production code. There are many abstraction techniques (we will use ValueObjects ) in order to avoid duplicating. But we will look at it in my third article of the TDD series. Stay updated and don’t miss it!

Learn about the ins and outs of open source security and management.

Topics:
tdd ,unit testing ,refactoring ,test driven development ,devops ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}