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.

smci
  • 139

10 Answers10

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.

Telastyn
  • 110,259
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.

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.

user949300
  • 9,009
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.

Robert Baron
  • 1,132
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).

wjl
  • 267
Joppe
  • 4,616
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.

Ivan
  • 575
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.

Ant P
  • 833
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.

o.m.
  • 505
-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.