TDD: primitive obsession ( part 3 )

Share This Post

Last month we talked about TDD example in software development ( part 1 ) and TDD first cycle ( part 2) . In this new TDD and primitive obsession article, we will focus on removing duplication and reinforcing the constructors of our entities, something key to have a robust system.

 

TDD: primitive obsession

 

Here comes an interesting DDD concept – the use of ValueObjects and the removal of Primitive Obsession from the head. A String is not a name, a name is a subset of String (Refined Types). In our ubiquitous language created with client, there is no string, there is a Name or Last Name.

Within the String set exists “”, “4”, “<script src =” malicious.js “/>” and everything you can imagine, a Name is the same? We can not ignore this logic and with the Surname happens, the same, and we do not want to duplicate code …

Let’s start with the Name ValueObject:


export class Name {
  constructor(private value: string) {
    if (value === "") {
      throw new Error(`Name should not be empty`)
    }
  }
}
describe("given Name value object", () => {
  const nameGenerator = (value: string) => new Name(value);
  const emptyValueName = () => nameGenerator("");
  const validName = nameGenerator("Oscar");
  describe("with valid value", () => {
    test("should not throw", (done: any) => {
      expect(validName.toDTO()).toEqual("Oscar");
      done();
    });
  });
  describe("when empty value", () => {
    test("should throw", (done: any) => {
      expect(emptyValueName).toThrow();
      done();
    });
  });
});

We have a ValueObject that comes from business, we have its validation with its tests in green as we have learned but our user does not know Name for now, we have to make him know it:


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

Perfect, now the test is OK, because we have passed a string and now it is a Name, do we really have to modify it? Have we done something wrong along the way? Well, the answer is no, we are not just going to modify our function that the user creates (remember that in the previous refactor we removed it to a function and we will only have to change it in one place) but above, WE WILL ELIMINATE TEST! No need to test that it throws an exception when the name is empty because it directly enters a Name, the Name already does that validation, the rest is done in compilation! The new test code would look like this:


const createUser = (name: string, lastname: string) => new User("someId", new Name(name), lastname);

Oh! What kind of unit is this in which User receives the correct Name? Well, if you’ve come this far without reading the UnitTest link from Fowler, it’s time to do it, when you finish you’ll understand everything (on top of that you’ll like it, I promise).

We have refactored Name but we need Lastname, let’s do it now that we have everything in green. This case is quite easy because it is the same as Name! Copy & Paste time!


// test/LastNameSpec.ts
export class LastName {
  constructor(private value: string) {
    if (value === "") {
      throw new Error(`LastName should not be empty`)
    }
  }
  toDTO() {
    return this.value;
  }
}
describe("given lastname", () => {
	const lastNameGenerator = (value: string) => new LastName(value);
	const emptyValueLastName = () => lastNameGenerator("");
	const validLastName = () => lastNameGenerator("Oscar");
	describe("with valid value", () => {
	  test("should not throw", (done: any) => {
	    expect(validLastName).not.toThrow();
	    done();
	  });
	});
	describe("when empty value", () => {
	  test("should throw", (done: any) => {
	    expect(emptyValueLastName).toThrow();
	    done();
	  });
	});
});
// src/User.ts
export class User {
  constructor(private id: string, private name: Name, private lastname: LastName) {}
}
describe("given user", () => {
  const createUser = (name: string, lastname: string) => new User("someId", new Name(name), new LastName(lastname));
  const createValidUser = () => createUser("Oscar", "Galindo");
  const createUserWithEmptyName = () => createUser("", "Galindo");
  const createUserWithEmptyLastName = () => createUser("Oscar", "");
  describe("with valid data", () => {
    test("is created", (done: any) => {
      expect(validUser.toString()).toEqual("User(someId,Oscar,Galindo)");
      done();
    });
  });
  describe("with empty name", () => {
    test("should throw", (done: any) => {
      expect(createUserWithEmptyName).toThrow();
      done();
    });
  });
  describe("with empty lastname", () => {
    test("should throw", (done: any) => {
      expect(createUserWithEmptyLastName).toThrow();
      done();
    });
  });
});

I would say that the next phase (REFACTOR) is very important but the thing is that all three phases are important. It is crucial to know which phase we are in and focus on it. We see that Name and LastName have exactly the same logic of validation, we can extract it to a more abstract class (let’s call it NonEmptyString) and that Name and LastName extend from it:


export class NonEmptyString {
  constructor(protected value: string) {
    if (value === "") {
      throw new Error(`${this.constructor.name} should not be empty`)
    }
  }
  toDTO() {
    return this.value;
  }
}
export class Name extends NonEmptyString {}
export class LastName extends NonEmptyString {}
describe("given nonEmptyString value object", () => {
  describe("when create with valid data", () => {
    test("should not throw", (done: any) => {
      const validEmptyString = () => new NonEmptyString("some random string");
      expect(validEmptyString).not.toThrow();
      done();
    });
  });
  describe("with empty value", () => {
    test("should throw", (done: any) => {
      const invalidEmptyString = () => new NonEmptyString("");
      expect(invalidEmptyString).toThrow();
      done();
    });
  });
});

Very well, at the level of production code we already have an interesting refactor as well as tests that are in green. What we would need to refactor – are the tests, we have a couple of tests that are duplicated, thanks to this refactor we can eliminate two tests which are the creation of user with invalid first and last name, why? Because our User class no longer understands strings, it understands Name and LastName, and what about those two entities? They do not exist if they are empty, it is impossible for someone to build those entities with an empty value, we already have it controlled, the one that a user builds well no longer depends on our test but on the compiler, cool, right ? Less test, less maintenance and our code has gained readability.

  TDD example in software development (Part I)

Now, along the way we have been seeing how we were dragging an ID as a string in our Entity, now that we understand the concept of which Name is not a string but a Name, we can think that the ID property is the same, we have to validate that the ID has the format that we require, etc.
We know that an entity is identified by its id, then, if we compare an entity with itself, they should be the same, right? let’s write the test:


describe("when compare same entity", () => {
	test("should be equal", (done: any) => {
	  const user = User.create("Oscar", "Galindo");
	  expect(user.equals(user)).toBeTrue();
	  done();
	});
});

At this point we have created two static methods in the User class, one to create and another to instantiate (register a new user or instantiate one previously registered), with the only difference that one receives ID and the other does not.


export class User {
  static create(name: string, lastname: string): User {
    return new User(v4(), new Name(name), new LastName(lastname));
  }
  static new(id: string, name: string, lastname: string): User {
    return new User(id, new Name(name), new LastName(lastname));
  }
}

We could think about using Obvious Implementation since the implementation is familiar and simple but I know we have to do two tests, one when two users are the same and another for the situation when they are different, so we will go step by step, which costs nothing:


export class User {
  ...
  equals(other: User): boolean {
    return true;
  }
}

We already have the test in green, we create the following test that will cover all the cases of the method, we put it in green and ready:


// userSpec.ts
describe("when compare two differents users", () => {
	test("should return false", (done: any) => {
	  const user = User.create("Oscar", "Galindo");
	  const otherUser = User.create("Pedro", "Garcia");
	  expect(user.equals(otherUser)).toBeFalse();
	  done();
	});
});
// user.ts
export class User {
  ...
  equals(other: User): boolean {
    return this.id === other.id;
  }
}

We enter the phase of Refactor, in this phase we not only remove duplication, we also design our application within this space that TDD provides us.

  Developer Experience Infrastructure Components

A User knows that to compare itself with another User it would be enough to compare the identifiers (id) but, is it the User‘s responsibility to do the concrete implementation of that equals? The best solution would be that the identifier is the one that is compared with another identifier, that is, we have to convert id into a Value Object.

We are going to create UserId in the same way that we have created Name, what we are going to do is create a class with two static methods to generate / instantiate UserId and implement the equals within our new class, we need to create a test for the equals:


export class UserId extends NonEmptyString {
  static new(id: string): UserId {
    return new UserId(id)
  }
  static generate(): UserId {
    return new UserId(v4());
  }
  equals(otherUserId: UserId): boolean {
    return this.value === otherUserId.value;
  }
}
describe("given UserId", () => {
  describe("when compare with same userId", () => {
    test("should return true", (done: any) => {
      const userId = UserId.new("someId");
      expect(userId.equals(userId)).toBeTrue();
      done();
    });
  });
  describe("when compare with other userId", () => {
    test("should return false", (done: any) => {
      const userId = UserId.new("someId");
      const otherUserId = UserId.new("otherId");
      expect(userId.equals(otherUserId)).toBeFalse();
      done();
    });
  });
});

And in our class we can change the implementation of the equals to something like this:


export class User {
  ...
  equals(other: User): boolean {
    return this.id.equals(other.id);
  }
}

All tests are still green with this refactor, we have achieved a better design and more decoupled in a short period of time.

At this point, what we are looking for is to create a Note, so let’s think, who will create the note? how? what data is needed ? It is important to understand that entities do not usually create themselves, you have to know where they should be created from. A note is not created alone, it is created by a user, so it is the user who will necessarily have a createNote method that will generate a Note, we will create the test of our use case:


describe("when create a note", () => {
  test("should return a note", (done: any) => {
    const authorId = UserId.new("someAuthorId");
    const user = User.new(authorId, "Oscar", "Galindo");
    const note = user.createNote("Some text");
    expect(note.toString()).toContain(`someAuthorId,Some text`);
    done();
  });
});

Now we have broken our tests in all possible ways, Note does not exist, createNote is not a method of the User class and on the other hand, the test we do is that the toString () contains both our authorId and the text of the note , we can not compare it with a specific note because NoteID is not deterministic, and we don’t have a way to know that id is going to be generated in our new note. These are some of the ways to test these cases:

  • Accessors to the properties. One way is by testing the properties of the note that you have created, but I think that by doing getters to the properties, we break the encapsulation or we start doing it (the encapsulation is broken when you access properties so that from outside we apply certain behavior, not when we make a property public).
  • That every entity knows how to become its DTO. Convert the entity to its DTO and test its properties, at this time we do not have DTOs in our application and test them is somewhat cumbersome.
  • Controlling the way in which ids are generated, with the current implementation we have fully coupled to the uuid library, if we made a wrapper of the library, we could do a wrapper stub and know at all times what ID is generated and we would have the determinism that we seek when we test and verify that the Note created by the user is exactly the same as one that we can create in the test.

I read this last strategy in The Art Of Unit Testing and it applies for the DateTime, basically what we do is remove the indeterminism from the equation when generating a new Id tsaying that it has to return. We create a static generator property where it has assigned by default the library that we use to generate the id’s and replace it with a function that returns a string that we know.

Once the test is explained, we will start to implement our use case, we will create the method within User that returns a new Note.

A Note is an entity and every entity is identified by an ID, exactly like User. In this case, Note will only have one property (removing its identifier) that will be the text of the note (text):


export class NoteId {
  constructor(private value: string) {}
  static generate() {
    return new NoteId(v4());
  }
}
export class Note {
  constructor(private noteId: NoteId, private authorId: UserId, private text: string) {}
  static create(authorId: UserId, textNote: string): Note {
    return new Note(NoteId.generate(), authorId, textNote);
  }
  static new(id: string, authorId: UserId, text: string): Note {
    return new Note(new NoteId(id), authorId, text);
  }
}
export class User {
  ...
  createNote(textNote: string): Note {
    return Note.create(this.id, textNote);
  }
}

We already have a User that knows how to create Note, all tests in green … now, it is time for refactoring. Two rules that we can follow (for me main ones) in this phase of the TDD is to look for repeated code in the same class and repeated code in different classes (possible abstraction). (TDD Checklist)

  Legacy Code: definition, recommendations & books

Inside the Note class we do not have any type of duplication but if there is duplication between the NoteId and UserId classes, they are ValueObjects that identify an entity, we can abstract an EntityId class and extend NoteId and UserId of it.


export abstract class EntityId extends NonEmptyString {
  equals(entityId: EntityId): boolean {
    return this.value === entityId.value;
  }
}
export class UserId extends EntityId {
  static new(id: string): UserId {
    return new UserId(id)
  }
  static generate(): UserId {
    return new UserId(v4());
  }
}
export class NoteId extends EntityId {
  static new(id: string): NoteId {
    return new NoteId(id)
  }
  static generate(): NoteId {
    return new NoteId(v4());
  }
}

Conclusion: TDD and primitive obsession 

In this chapter we have seen how to reinforce the constructors of our entities taking advantage of the Value Objects. We have eliminated tests since the compilation did the rest and we have removed duplication, this is the process that must be followed in a true TDD development, continuous refactor . The refactor of our application is not optional, together with an evolutionary design we get robust applications that work.

In the next chapter we will enter the world of persistence, how we will do TDD with infrastructure issues, uncouple as much as possible from the database we use, etc.

 

If you would like to know more about TDD and primitive obsession, I highly recommend you to subscribe to our monthly newsletter by clicking here.  

 

And if you found this article about TDD and primitive obsession interesting, you might like…

 

TDD example in software development ( part 1 )

TDD: first cycle ( part 2 )

Scala generics I: Scala type bounds

Scala generics II: covariance and contravariance  

Scala generics III: Generalized type constraints

BDD: user interface testing

F-bound over a generic type in Scala

Microservices vs Monolithic architecture

“Almost-infinit” scalability

iOS Objective-C app: sucessful case study

Mobile app development trends of the year

Banco Falabella wearable case study 

Mobile development projects 

Viper architecture advantages for iOS apps 

Why Kotlin ? 

Software architecture meetups

Pure MVP in Android 

Be more functional in Java ith Vavr

 

Author

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange