DEV Community

Adrián Norte
Adrián Norte

Posted on

You don't know TDD

Okay, maybe you think you know what TDD is but let me tell you a story.

Some years ago there was this young developer who thought that, even when she/he had a lot to learn, she/he at least got a pretty solid foundation. She/He knew that coupling is bad and classes should be single responsibility and the methods inside the class should be also single responsibility in order to produce maintainable and extensible software.

To this point, most of us can resonate with this story and laugh at ourselves for being that naive. Of course, we think we write decoupled code now but the truth is most developers don't know what TDD is and they write coupled code, less coupled that some years ago because of practice, but coupled none the less. Proof of that is on your companies repositories and how developers shudder every time they need to change something on the codebase.

TDD is not Unit Testing but Unit Testing is TDD.

TDD, as the name implies, is the practice of developing software aided by tests. One of the tools are Unit Tests but if you only use those you get something like this:

Integration tests serve the purpose of checking that your code makes overall sense and does what is expected to do.

So, what purpose Unit Tests serve?

Unit Test most people think they exist to ensure the contract between classes and they are right but those tests also allow the developer to ensure the quality of the code by listening to the test.

Listening to the tests.

A Unit Test shouldn't take more than three or five lines of code and a couple of minutes to write, assuming there is no accidental complexity. If your test is long or hard to write it is a signal of coupling.

Tests usually are kinda like "given X to ClassA.cat() it will call ClassB.fox() with X*Y" and then you write the call and an assertion, depending on which language and framework this should take two or three lines. If ClassA.cat() does more than multiply then your test will become harder and harder the more things ClassA does because you need to come up with the output to assert against and if your code does a lot of things it would become more difficult to ensure the assertion value.

Applying TDD to the daily code.

Red green refactor is great but is not for everyone neither for all the scenarios.

Here is how I do things: I write a test that says "ClassA.cat() calls ClassB.fox()". I call those test the design tests and use them to build the general structure of the classes. Then, I write the ones that specify the values received and the expected outputs and design the detail of which class does what, in this phase usually the design test get modified because there was some hidden coupling.

After those happy test, I write some tests to handle errors and some with other values to ensure nothing weird happens. Then, I write integration tests.

And that is more or less it. Also, sorry for my English.

Latest comments (43)

Collapse
 
n_develop profile image
Lars Richter

Hi Adrián,
thanks for your post.
I'm always amazed by how emotional these TDD discussions get. It reminds me very much of the current "replace master with main" discussion. There are stubborn opinions for and against it.

I don't like hardliner mindsets in general. There a people hunting the 100% code coverage which leads to silly tests. But there are the "don't write any test at all" people as well. I think tests are super valuable. But you have to learn to write useful tests.
At work, we try to write tests for all the important parts of our (pretty big) application and it works out pretty well for us.

Collapse
 
sobhan_1995 profile image
Sobhan

whats is difference between tdd and unit test?

Collapse
 
anortef profile image
Adrián Norte

TDD is a way to develop using tests while unit tests are tests that aim to check and verify the smallest unit of your logic.

Unit testing is part of TDD but only a part, you need integration tests also.

Collapse
 
610yesnolovely profile image
Harvey Thompson

I prefer to just think of tests as tests. I also don't often mock classes.

I write tests first to prove what I'm about to code is correct (or occasionally that my mental model in testing doesn't match reality, or the class API sucks).

Typically software is built in layers, so for higher levels, I can write tests assuming the lower levels are tested (because they have tests) and work (because the code runs and does something sensible also).

Not usually important to mock because that's a lot of work that provides little benefit and slows progress. I do mock if it's easy to do (abstract base classes) or very important to hide some super complex/fragile system.

I try to balance forward progress, rather than testing everything, especially important because half the code gets rewritten so much that I'd have to rewrite half the tests. "Just enough tests" is actually therefore better than "everything is tested", which is better than "Not enough or no tests".

Collapse
 
adamluzsi profile image
Adam Luzsi • Edited

Test behavior not implementation is my motto. I like cleanly separate the external resources and test them with integration tests that reused as a shared specification to implement in memory representation. With that being done, I don't have to use any mock/stub anymore, and my tests only aim to do behavior testing through composition.

Of course this requires a small amount of self discipline, but so far (in the past years) I only have good experiences with it.

The only "con" with it that each time an external resources needs to be added to the system, there is an extra step for creating an in memory implementation as well by shared specs.

But I'm a simple man. I see post about TDD, I upvote :)

Collapse
 
timberzen profile image
doug

Personally TDD means your code is testable. It's designed, that it's proven to be testable, at a smaller level. The tests also communicate on how it works. It also speeds up development, develop once and park it. I do reactor, but it can be an obsession, visually I want to keep it simple (KISS), I hate VAR in code reviews as I dont want the cognate overload of working out what VAR is especial when there is so much to scan. Sure there is the issue that some people don't get anything out of tests, then don't do testing. For me it's also pain reduction. The next question is about mocking and fakes, what is testable and is the fake honest? Next, are the tests giving me a false sense of security? Finally you are delivering a product, at a price point, tests are not the delivery, you code is (YAGNI).

Collapse
 
nestedsoftware profile image
Nested Software • Edited

Tests usually are kinda like "given X to ClassA.cat() it will call ClassB.fox() with X*Y"

In my opinion, this is a bad idea. A test should verify that a given function does what it was meant to do. Most of the time, the details of its implementation should not be part of the test. The approach you describe will make it easy to create passing tests even though the actual application logic doesn't work properly. It's also brittle: If you change the implementation of a function, the tests are likely to break, even if the contract that function fulfills remains unchanged.

There are cases where this kind of testing, using mocks, is appropriate. If your function interacts with an external system, then there is a decent chance that you should mock that out for testing purposes. For instance, a test may say "please make sure that the send_text_message function was called with the following parameters" -- but without actually running send_text_message. This is done when the external system may be unavailable entirely, or produces results that vary over time, or would slow down the test suite too much.

Over all though, I recommend that a test should call a given function, and then confirm that the return value of that function is what was expected, or at least that some state change resulting from that function call is what it should be.

Collapse
 
serhuz profile image
Sergei Munovarov

DAL/DAL/REPO layer mustn't be mocked. Why?. Simply, the objective is to test a function that works with the database.

But you could just test your DAOs/repositories with, say, in-memory DB instance in isolation. And nothing prevents you from using mocks with other application-level tests.

Collapse
 
stilldreaming1 profile image
still-dreaming-1 • Edited

I would still be worried that the in memory database does not behave the same, and this would hide many bugs that would otherwise be caught by using the real database engine.

Collapse
 
ghy profile image
skrc8

I don't like those type of tests, since the one you are talking about is tied to the implementation.

As you say that's good to put clear contracts between classes, but I don't see the purpose of that even when I think having clear interfaces is important.

Instead I prefer to test how each function answers with different parameters and edge cases.

Thanks for sharing and making me think about testing.

Collapse
 
stilldreaming1 profile image
still-dreaming-1 • Edited

First I want to clarify that I do strongly believe in using automated testing and feel TDD is a great tool.

A programming article with no examples is perfectly fine, but if you are going to do that you should get a little more deep in a philosophical sense, and question your own reasoning more. The idea that coupling is bad makes no logical sense, it is exactly the same as saying "using code is bad", which is the name of an article I wrote that explains this (also with no examples).

As evidence that I am correct, the subsequent behavior you promote in an attempt to remove coupling is crazy. You say unit tests should test which methods on other objects the class under test calls, but that is not testing the contract at all, it is testing and duplicating the implementation.

The contract is the expected behavior of using the interface the class provides. If the expected behavior of an object is that it calls certain methods a certain way on other objects, then all you have done is implement your own harder to use programming language within the language you are using. But that kind of behavior is only the natural consequence of trying to avoid coupling.

Collapse
 
anortef profile image
Adrián Norte

Coupling is bad because it increases the amount of code impacted with any minor change, therefore increasing the cost of maintenance.

You say unit tests should test which methods on other classes it calls, but that is not testing the contract at all, it is testing and duplicating the implementation.

I said also, that you should test how they do these calls and explain how that helps reduce coupling. I don't know how you write your tests but if you need to duplicate the implementation I suggest a change.

Collapse
 
stilldreaming1 profile image
still-dreaming-1 • Edited

Once again you are not fully thinking through the words and concepts you are using. All code is coupling. The entire point of code is coupling. If you remove all coupling, you no longer have a system, just a bag of objects. You have it backwards. I can repeat your first sentence as the exact opposite, and it makes more sense. Coupling is good because it increases the amount of code impacted with any minor change, thereby decreasing the cost of maintenance. Good classes achieve high conceptual compression, not abstraction.

Thread Thread
 
anortef profile image
Adrián Norte

So, you are talking about the coupling vs cohesion thing.

TDD can help with that too, If you listen to the tests, of course. Like with coupling, if you detect your tests being hard to do, you can detect lack of cohesion if you realize that most of your tests just check that X calls Y with exactly the same output as input was given.

Thread Thread
 
stilldreaming1 profile image
still-dreaming-1 • Edited

Well, I guess we will have to agree to disagree. I like that you brought cohesion into the picture, as it is one part of getting compression right. I also like that you have been talking about listening to your tests. It is important to listen to your tests, and your code in general, as it can talk and give feedback to those who know how to listen. Ultimately though I feel that coupling is a good thing, and I'm not sure how writing the types of unit tests you describe would help me find the unwanted types of coupling. To me the only coupling I don't want is random coupling, which I would automatically avoid just by not using random extra things in the code, by having a very general aesthetic sense of what the responsibilities of a class should and should not be, and by refactoring to simplify things (although that simplification is often accomplished by either introducing some new coupling or by replacing some existing coupling with more desirable coupling).

Collapse
 
detunized profile image
Dmitry Yakimenko

I think it would be easier to follow if you've demonstrated this on a you example that you develop step by step in your article.

Collapse
 
anortef profile image
Adrián Norte

Good idea! I will do a followup of this one with that when I have time. Thanks :D