6 Comments
Sep 18Liked by Valentina Jemuović

I'm working on a legacy project where the business logic code and infrastructure code is coupled. It's not possible to write unit tests in this case?

Expand full comment
author

That's a common design problem with legacy code. This makes Unit Test impossible, hence it's also impossible to apply the TDD cycle at the Unit Test level. To solve this, I recommend you first write Component Tests, these can be written even for a Big Ball of Mud. Then, they will provide you with protection so that you can redesign your classes to decouple the business logic and infrastructure code into separate classes, and then you'll be able to unit test the business logic. You can then apply TDD for changes to that business logic.

Expand full comment

I find it challenging to implement application-specific use cases using classic TDD, and even more challenging to sell it to team mates. Let's say we have a use case where a user can buy a concert ticket. In simple terms, the algorithm would look something like "fetch user by id, fetch free ticket, add ticket to user, store updated user, present result". In classic TDD, as I understand it, I would start with no user and no ticket, 1 ticket added to 1 user, N tickets added to 1 user, and then N tickets added to N users, constantly refactoring and extracting abstractions to adapt the design to the new reality. In the case of fetching a free ticket, I'd probably first have no field in the use case class, then a field for 1 ticket, then a field for a list of tickets, then I'd wrap this list in a TicketRepository abstraction, and finally I'd extract the interface for this TicketRepository. This seems kind of counter intuitive - if I already know that I will need multiple users and multiple tickets, why would I start with 0, 1, N? I'd rather start with fetching users and tickets directly?

Expand full comment
author

That's true, in classic TDD, we'd start of with 0, 1, N (and that's something shown in Uncle Bob's materials). I do find it useful for problems like FizzBuzz and Fibonnaci sequence, which are really algorithmically complex in nature. Most of Uncle Bob's examples are in that category - problems which are really algorithmically useful.

But when it comes to problems in the real world, it's like the example you gave - fetch entity from repository, call some method on entity to update the entity state, save the updated entity in the repository, return some result to the caller... This simple algorithm is so common. Here we immediately know the algorithm implementation, because it's just so common/templated. Then in that case, going the incremental way would help us firstly setup the existence of classes and interfaces (e.g. setting up existence of User class, Ticket class, TicketRepository interface, FakeTicketRepository class) and some basic methods... so basically designing the interface of each of those, without real implementation inside the method... and then in subsequent iterations it causes us to implement the true methods inside and also add data to the entities.

"why would I start with 0, 1, N" -> I've noticed that sometimes when someone jumps to the N case immediately, they might not handle the 0 case in the test at all.. for example, there's no test regarding the case that user doesn't exist in the repository and we're attempting to buy tickets for them. It means we missed a requirement. Or maybe we did implement it in code, but didn't add the test for that boundary case - that means we have lower mutation score - our tests are not protecting us...

I see two possibilities:

1. Proceed in the incremental way: 0, 1, N

2. Immediately jump to the end solution instead of the little steps, esp. when you know exactly what you'll implement and the tests, and then do run mutation testing just to make sure that your tests and code are both aligned on boundary conditions too...

Hence I see your suggestion as a valid approach, something I came to thinking... It's like TDD, but with a bigger step. With teams, I'd start with Approach 1, later may do Approach 2 but with checking from Mutation Testing.

Your question is a difficult one to answer, I may come back to it later with code examples perhaps...

Expand full comment

I believe one of the biggest challenges is convincing the entire team to adopt TDD and embrace it collectively. It would be great to read more about strategies for promoting TDD within a team.

Expand full comment
author

Yes, that's a common challenge. For example, an individual developer wants to practice TDD, that requires that the existing codebase be written in a testable way. If the other developers aren't doing that, then when we modify that part of the codebase, we'd have to do restructuring of it before apply TDD. It would be much better to have the whole team onboard.

A useful strategy to promote TDD is start small. When I coach teams, I firstly help them transition their existing Test Last Development to do it well - to refactor the exist test suite, and learn how to write tests (at different levels) effectively. Then when they have skillsets in writing proper tests (and also skillsets in refactoring), then it's more motivating for them to try TDD. It can be tried as an experiment, on one user story, and see how it goes.

Expand full comment