2

I'm learning and trying TDD recently, and I constantly encounter a situation when I need to ditch my current test because it's too broad. I don't know how to deal with it. To be more specific, let's suppose I have a class User, which contains a method getInfo. To follow TDD, I need to write my test first:

@Test
public void test_get_info() { ... }

Then implement my method:

class User {
    public UserInfo getInfo() { ... }
}

When I'm implementing the method, I suddenly find out that there are two kinds of users, one is Employee, the other is manually-registered user, one test is not enough to cover both. To get info for both kinds of users, I need to create two more classes:

class Employee extends User {
    public UserInfo getInfo() { ... }
}

class RegisteredUser extends User {
    public UserInfo getInfo() { ... }
}

Before implementing it, I have to write two tests one by one for Employee.getInfo and RegisteredUser.getInfo. But suppose when I've finished writing the test for Employee.getInfo and try to implement it, I find out that UserInfo contains user name and user address, both have complicated logic and deserve a separate test case. Thus, before implementing Employee.getInfo, I need to write tests for UserInfoFetcher.getUserName and UserInfoFetcher.getUserAddress...

Have you seen the problem? To pass the first test, I need to write multiple other tests and pass them before the first one. It doesn't seem like TDD, which usually demands one test each time. How do you solve the problem?

6
  • 2
  • 2
    What you are doing is essentially refactoring your API as you go. This is a very good thing. Being able to call your code and get something meaningful out is very important. Dec 18, 2021 at 18:32
  • This happens because TDD is antiethical to design. Embrace design, or embrace TDD: Your choice.
    – davidbak
    Dec 18, 2021 at 23:50
  • @davidbak: I think your comment is missing the point. TDD and design are mostly orthogonal, they are not at odds (note I am not agreeing to those TDD zealots who say TDD will embrace good design, that's almost the same misconception). Please take the time to read my answer below, then you willl probably understand the real problem here,
    – Doc Brown
    Dec 19, 2021 at 7:45
  • 1
    @davidbak: my answer does not say anything about TDD and design, because that is not the problem the OP has.
    – Doc Brown
    Dec 19, 2021 at 19:47

2 Answers 2

8

I voted to close this question as a duplicate, because I think this case can be solved by applying this answer, but let me explain it a little bit more in-depth by using your specific example.

The problem vanishes by working in smaller steps:

  • First make test_get_info pass by a really simplistic implementation of getInfo. For this, test_get_info shouldn't try to test something complicated, only something you can resolve with a few lines of code. The fact you "suddenly find out that there are two kinds of users" should not stop you from implementing getInfo for a simple User up to a point it can be compiled and tested.

  • Even when you know you need two types of users, add only a test for Employee.getInfo and make that pass by adding the new Employee class (still using a simplistic implementation, maybe one which reuses and/or overloads User.getInfo). Don't add two tests at a time, like you described it!

  • Do the same for RegisteredUser.getInfo.

  • Don't forget to refactor in between (the tests will help you to make sure you don't break anything).

  • Now you are planning to replace the oversimplistic logic by the real one. As soon as you realize you need this new method UserInfoFetcher.getUserName, write a test for it, then implement it (but don't change any getInfo implementation at that point in time).

  • Then add a test test_get_info2 for getInfo which cannot be passed by the simplistic implementation any more. Getting this test pass will require to change getInfo in a way it uses the available getUserName, which is now just a "baby step".

  • refactor further, run the tests again...

In reality, you may choose a different order to work here, but I guess you get the idea. If it helps, you can first scetch test_get_info2 before working at getUserName, but leave it deactivated / in comments until you got getUserName ready (which is what the top answer from the dupe suggests).

2

Personally I mostly write the tests at a higher layer being the API meaning I avoid the implantation detail like this.

The way your tests interact with the code creates a contract that is hard to change later. Given the API is an existing contract that you don't want to change its quite stable. Stub out external services and use an in memory db and they can be reasonably fast, although still relatively slow vs a pure unit test.

I started TDD focusing on unit tests but slowly realised I often missed bugs due to them being higher up or in how the components connected. Writing tests for the higher layers often felt like I was duplicating the setup of the other tests. Any refactor often meant throwing away many tests and writing them from scratch instead.

1
  • 1
    I think this is correct. The fundamental problem is that refactoring happens and if your tests are at the lowest level possible and thereby completely tied to the design, you often have to throw them out. Better to prefer testing at a higher level, which while harder, allows tests to help a refactor rather than hinder it.
    – Nathan
    Dec 28, 2021 at 9:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.