43

Our colleague promotes writing unit tests as actually helping us to refine our design and refactor things, but I do not see how. If I am loading a CSV file and parse it, how is unit test (validating the values in the fields) gonna help me to verify my design? He mentioned coupling and modularity etc. but to me it does not make much sense - but I do not have much theoretical background, though.

That is not the same as the question you marked as duplicate, I would be interested in actual examples how this helps, not just theory saying "it helps". I like the answer below and the comment but I would like to learn more.

10
  • 7
    Possible duplicate of Does TDD lead to the good design?
    – gnat
    Feb 9, 2017 at 18:15
  • 3
    The answer below is really all you need to know. Sitting next to those folks who write aggregate-root dependency-injected factory factory factories all day long is a guy who quietly writes simple code against unit tests that functions correctly, is easy to verify, and is already documented. Feb 9, 2017 at 18:47
  • 4
    @gnat doing unit testing does not automatically imply TDD, it's a different question
    – Joppe
    Feb 9, 2017 at 19:29
  • 11
    "unit test (validating the values in the fields)" - you appear to be conflating unit tests with input validation.
    – jonrsharpe
    Feb 9, 2017 at 19:32
  • 1
    @jonrsharpe Given that it's code parsing a CSV file, he may be talking about a real unit test that verifies a certain CSV string gives the expected output.
    – JollyJoker
    Feb 10, 2017 at 11:46

10 Answers 10

3

Not only do unit tests facilitate design, but that is one of their key benefits.

Writing test-first drives out modularity and clean code structure.

When you write your code test-first, you will find that any "conditions" of a given unit of code are naturally pushed out to dependencies (usually via mocks or stubs) when you assume them in your code.

"Given condition x, expect behaviour y," will often become a stub to supply x (which is a scenario in which the test needs to verify the behaviour of the current component) and y will become a mock, a call to which will be verified at the end of the test (unless it's a "should return y," in which case the test will just verify the return value explicitly).

Then, once this unit behaves as specified, you move on to writing the dependencies (for x and y) you have discovered.

This makes writing clean, modular code a very easy and natural process, where otherwise it's often easy to blur responsibilities and couple behaviours together without realising.

Writing tests later will tell you when your code is poorly structured.

When writing tests for a piece of code becomes difficult because there are too many things to stub or mock, or because things are too tightly coupled together, you know you have improvements to make in your code.

When "changing tests" becomes a burden because there are so many behaviours in a single unit, you know you have improvements to make in your code (or simply in your approach to writing tests - but this is not usually the case in my experience).

When your scenarios become too complicated ("if x and y and z then...") because you need to abstract more, you know you have improvements to make in your code.

When you end up with the same tests in two different fixtures because of duplication and redundancy, you know you have improvements to make in your code.

Here is an excellent talk by Michael Feathers demonstrating the very close relationship between testability and design in code (originally posted by displayName in the comments). The talk also addresses some common complaints and misconceptions about good design and testability in general.

1
  • @SSECommunity: With only 2 upvotes as of today this answer is very easy to overlook. I highly recommend the Talk by Michael Feathers that has been linked in this answer. Feb 14, 2017 at 15:02
103

The great thing about unit tests is they allow you to use your code how other programmers will use your code.

If your code is awkward to unit test, then it's probably going to be awkward to use. If you can't inject dependencies without jumping through hoops, then your code is probably going to be inflexible to use. And if you need to spend a lot of time setting up data or figuring out what order to do things in, your code under test probably has too much coupling and is going to be a pain to work with.

10
  • 7
    Great answer. I always like to think of my tests as the first client of the code; if it's painful to write the tests, it will be painful to write code that consumes the API or whatever that I'm developing. Feb 9, 2017 at 22:49
  • 41
    In my experience, most unit tests do not "use your code how other programmers will use your code.". They use your code as unit tests will use the code. True, they will reveal many serious flaws. But an API that is designed for unit-testing may not be the API that is most suitable for general use. Simplistically written unit tests often require the underlying code to expose too many internals. Again, based on my experience - would be interested in hearing how you handled this. (See my answer below)
    – user949300
    Feb 10, 2017 at 0:15
  • 7
    @user949300 - I'm not a big believer in test first. My answer is based on the idea of code (and certainly design) first. APIs shouldn't be designed for unit-testing, they should be designed for your customer. Unit-tests help approximate your customer, but they're a tool. They're there to serve you, not vice versa. And they're certainly not going to stop you from making crappy code.
    – Telastyn
    Feb 10, 2017 at 0:38
  • 3
    The biggest problem with unit tests in my experience is that writing good ones is just as hard as writing good code in the first place. If you can't tell good code from bad code, writing unit tests isn't going to make your code better. When writing the unit test, you have to be able to distinguish between what's a smooth, pleasant usage and an "awkward" or difficult one. They might make you use your code a little, but they don't force you to recognize that what you're doing is bad.
    – jpmc26
    Feb 10, 2017 at 0:53
  • 2
    @user949300 - the classic example I had in mind here is a Repository that needs a connString. Suppose you expose that as a public writable property, and have to set it after you new() a Repository. The idea is that after the 5th or 6th time you have written a test that forgets to take that step - and thereby crashes- you will be "naturally" inclined towards forcing connString to be a class invariant - passed in the constructor - thereby making your API better and making it more likely that production code can be written that avoids this trap. It's not a guarantee but it does help, imo. Feb 10, 2017 at 14:41
31

It took me quite a while to realize, but the real benefit (edit: to me, your milage may vary) of doing test driven development (using unit tests) is that you have to do the API design up front!

A typical approach to development is to first figure out how to solve a given problem, and with that knowledge and initial implementation design some way to invoke your solution. This may give some rather interesting results.

When doing TDD you have to as the very first write the code that will use your solution. Input parameters, and expected output so you can ensure it is right. That in turn require you to figure out what you actually need to have it do, so you can create meaningful tests. Then and only then do you implement the solution. It is also my experience that when you know exactly what your code is supposed to achieve, it becomes clearer.

Then, after implementation unit tests help you ensuring that refactoring doesn't break functionality, and provide documentation on how to use your code (which you know is right as the test passed!). But these are secondary - the greatest benefit is the mindset in creating the code in the first place.

13
  • That is certainly a benefit but I don't think it's the "real" benefit - the real benefit comes from the fact that writing tests for your code naturally pushes "conditions" out to dependencies and quashes over-injection of dependencies (further promoting abstraction) before it begins.
    – Ant P
    Feb 10, 2017 at 14:15
  • The problem is that you write a whole set of tests upfront that match that API, then it doesn't work exactly as needed and you have to rewrite your code and all the tests. For public facing APIs it's likely they won't change and this approach is fine. However, the APIs for code that is only used internally does change a lot as you figure out how to implement a feature that needs a lot of semi private APIs working together Feb 10, 2017 at 21:26
  • @AntP Yes, this is part of the API design. Feb 10, 2017 at 23:10
  • @JuanMendes This is not uncommon and those tests will need to be changed, just like any other code when you change requirements. A good IDE will help you refactor the classes as part of the work being done automatically when you change the method signatures etc. Feb 10, 2017 at 23:34
  • @JuanMendes if you're writing good tests and small units the impact of the effect you're describing is small to none in practice.
    – Ant P
    Feb 11, 2017 at 8:43
6

I would agree 100% that unit tests help "helps us to refine our design and refactor things".

I'm of two minds on whether they help you do the initial design. Yes, they reveal obvious flaws, and do force you to think about "how can I make the code testable"? This should lead to fewer side-effects, easier configuration and setups, etc.

However, in my experience, overly simplistic unit tests, written before you really understand what the design should be, (admittedly, that's an exaggeration of hard-core TDD, but too often coders write a test before they think much) often lead to anemic domain models which expose too many internals.

My experience with TDD was several years ago, so I'm interested in hearing what newer techniques might help in writing tests that do not bias the underlying design too much. Thanks.

1
  • Long number of method parameters are a code smell and a design flaw.
    – Sufian
    Feb 13, 2017 at 7:34
5

Unit test allow you to see how the interfaces between functions work, and often gives you insight as to how to improve both the local design and the overall design. Furthermore if you develop your unit tests while developing your code, you have a ready made regression test suite. It doesn't matter if you are developing a UI or a backend library.

Once the program is developed (with unit tests), as bugs are uncovered, you can add tests to confirm that the bugs are fixed.

I use TDD for some of my projects. I put a great deal of effort in crafting examples that I pull from textbooks or from papers that are considered correct, and test the code I am developing using these example. Any misunderstandings I have concerning the methods become very apparent.

I tend to be a bit looser than some of my colleagues, as I don't care if the code is written first or the test is written first.

1
  • That is a great answer to me. Would you mind to put a few examples, e.g. one for each case (when you get insight to design etc.).
    – User039402
    Feb 9, 2017 at 19:41
5

When you want to unit test your parser detecting value delimiting properly you may want to pass it one line from a CSV file. To make your test direct and short you may want to test it through one method that accepts one line.

This will automatically make you separate the reading of lines from reading individual values.

On another level you may not want to put all sorts of physical CSV files in your testing project but do something more readable, just declaring a big CSV string inside your test to improve readability and the intent of the test. This will lead you to decouple your parser from any I/O which you'd do elsewhere.

Just a basic example, just start practicing it, you'll feel the magic at some point (I have).

4

Put simply, writing unit tests help expose flaws in your code.

This spectacular guide to writing testable code, written by Jonathan Wolter, Russ Ruffer, and Miško Hevery, contains numerous examples of how flaws in code, that happen to inhibit testing, also prevent easy reuse and flexibility of the same code. Thus, if your code is testable, it is easier to use. Most of the "morals" are ridiculously simple tips that vastly improve code design (Dependency Injection FTW).

For example: It is very difficult to test if the method computeStuff operates properly when the cache starts evicting stuff. This is because you have to manually add crap to the cache until the "bigCache" is almost full.

public OopsIHardcoded {

   Cache cacheOfExpensiveComputations;

   OopsIHardcoded() {
       this.cacheOfExpensiveComputation = buildBigCache();
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

However, when we use dependency injection it is far easier to test if the method computeStuff operates properly when the cache starts evicting stuff. All we do is create a test in where we call new HereIUseDI(buildSmallCache()); Notice, we have more nuanced control of the object and it pays dividends immediately.

public HereIUseDI {

   Cache cacheOfExpensiveComputations;

   HereIUseDI(Cache cache) {
       this.cacheOfExpensiveComputation = cache;
   }

   ExpensiveValue computeStuff() {
      //DOES THIS WORK CORRECTLY WHEN CACHE EVICTS DATA?
   }
}

Similar benefits can be had when our code requires data that is usually held in a database...just pass in EXACTLY the data you need.

2
  • 2
    Honestly, I am not sure how you mean the example. How does the method computeStuff relate to the cache?
    – John V
    Feb 9, 2017 at 21:23
  • 1
    @user970696 -- Yes, I am implying that "computeStuff()" uses the cache. The question is "Does computeStuff() work correctly all the time (which depends on the state of the cache)" Consequently, it is hard to confirm that computeStuff() does what you want FOR ALL POSSIBLE CACHE STATES if you cannot directly set/build the cache because you hardcoded the line "cacheOfExpensiveComputation = buildBigCache();" (as opposed to passing in the cache directly via the constructor)
    – Ivan
    Feb 10, 2017 at 12:58
0

Depending on what is meant by 'Unit Tests', I don't think really low-level Unit tests facilitate good design as much as slightly higher level integration tests - tests that test that a group of actors (classes, functions, whatever) in your code combine properly to produce a whole bunch of desirable behaviours that have been agreed on between the development team and the product owner.

If you can write tests at those levels, it pushes you towards to creating nice, logical, API-like code that doesn't require lots of crazy dependencies - the desire to have a simple test setup will naturally drive you to not have lots of crazy dependencies or tightly-coupled code.

Make no mistake though - Unit tests can lead you to bad design, as well as good design. I've seen developers take a bit of code that already has a nice logical design and a single concern, and pull it apart and introduce more interfaces purely for the purpose of testing, and as a result make the code less readable and harder to change, as well as possibly even having more bugs if the developer has decided that having lots of low level unit tests means that they don't have to have higher-level tests. A particular favourite example is a bug I fixed where there was a lot of very broken-down, 'testable' code relating to getting information on and off the clipboard. All broken down and decoupled to very small levels of detail, with lots of interfaces, lots of mocks in the tests, and other fun stuff. Only one problem - there wasn't any code that actually interacted with the OS's clipboard mechanism, so the code in production actually did nothing.

Unit tests can definitely drive your design - but they don't automagically guide you to a good design. You do need to have ideas about what good design is that go beyond just 'this code is tested, therefore it's testable, therefore it's good'.

Of course if you're one of those people for whom 'unit tests' means 'any automated tests that aren't driven through the UI', then some of those warnings might not be so relevant - as I said, I think those higher-level integration tests are often the more useful ones when it comes to driving your design.

-2

Unit tests can help with refactoring when the new code passes all of the the old tests.

Say you have implemented a bubblesort because you were in a hurry and not concerned about performance, but now you want a quicksort because the data is getting longer. If all tests pass, things look good.

Of course the tests have to be comprehensive to make this work. In my example, your tests might not cover stability because that was no concern with bubblesort.

3
  • 1
    This is true but it's more a maintainability benefit than a direct impact on code design quality.
    – Ant P
    Feb 11, 2017 at 17:51
  • @AntP, the OP asked about refactoring and unit tests.
    – o.m.
    Feb 11, 2017 at 17:52
  • 1
    The question mentioned refactoring but the actual question was about how unit tests could improve/verify code design - not ease the process of refactoring itself.
    – Ant P
    Feb 11, 2017 at 17:53
-3

I've found unit tests to be most valuable for facilitating longer-term maintenance of a project. When I come back to a project after months and don't remember much of the details, running tests keeps me from breaking things.

1
  • 6
    That is certainly an important aspect of tests, but doesn't really answer the question (which is not why test are good, but how they influence design).
    – Hulk
    Feb 10, 2017 at 8:19

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