Table of Contents
Let’s continue with our series of TDD articles, in the first part we looked at the theory behind the TDD and Unit Testing. In this second part, TDD First Cycle, 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. Let´s start with our TDD first cycle process!
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, it can not 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 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. (The Art Of Unit Testing).
(*) 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. Let’s go back to the TDD First Cycle, we have to put that test in green in the simplest way possible and it may sound absurd, but that what must be done. TDD helps you to focus on one thing but of course, it depends on the programmer, hence 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 is the “User”, 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 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 part of the refactor. This part is special, I have heart 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, to model your code 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): 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: TDD Checklist and a great book Refactoring.
Let’s continue, we have the TDD first cycle finished. Based on the first test, I wonder if a user can exist with an empty name, 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 ToDo 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 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, 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 of name, we have to continue with the previous refactor for that, we already have a factory to create users, why not use it?
const createUserWithEmptyLastName = () => createUser("Oscar", "");
describe("with empty lastname", () => {
test("should throw", (done: any) => {
expect(createUserWithEmptyLastName).toThrow();
done();
});
});
Test in red, great, here the developer starts to lose a bit of focus and I repeat one of the 3 laws of TDD is not to write more code than enough amount 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 🙂 Here 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. Coming soon, stay updated and don’t miss it!
If you would like to know more about TDD first cycle, I highly recommend you to subscribe to our monthly newsletter by clicking here.
And if you found this article about TDD first cycle interesting, you might like…
TDD example in software development (Part I)
Scala generics I: Scala type bounds
Scala generics II: covariance and contravariance
Scala generics III: Generalized type constraints
F-bound over a generic type in Scala
Microservices vs Monolithic architecture
iOS Objective-C app: sucessful case study
Mobile app development trends of the year
Banco Falabella wearable case study
Viper architecture advantages for iOS apps
Be more functional in Java ith Vavr
Author
-
Software developer with over 16 years experience working as Fullstack Developer & Backend Developer.
View all posts