35

I'm a newbie to working in software development and I read a lot about how cool unit tests are. Now, I've made my first steps into a project where I'm working with a number of equally unexperienced programmers which is why we have produced a bit of spaghetti code. I'm trying to learn how to use testing and other techniques to improve the code quality but one of my newbie co-workers says that tests make things more difficult. Apparently, he has done internships in teams where unit tests were used.

He argued that tests were constantly in his way when he tried to implement a new feature. The tests would fail after he had changed the code. So he had to adapt the tests which of course increased his workload.

But that doesn't make sense to me. I thought tests were supposed to make things easier. So, I suspect that he either didn't implement the features correctly or that the unit tests were badly done. So, I'm wondering: How can I write unit tests so that they don't fail just because a new feature was implemented?

18
  • 23
    Having no tests makes it really easy to implement a feature. None of those pesky checks making sure you're not breaking code your boss wrote 5 years ago! Having tests and creating new tests or adapting existing tests to work with your newly implemented feature takes more work beforehand, but decreases your workload afterwards. This scales exponentially with project size and age, of course. Commented Jun 17, 2015 at 11:28
  • 3
    @k3b Hm, this is difficult. Yes, it is a duplicate. However, I looked for an answer using Google and the search at programmers.stackexchange and I didn't find this thread. I think the reason is that I haven't thought of the term 'fragile tests' as a synonym for 'tests that fail when new features are implemented'. This might be because I'm a newbie but I think there are more people like me out there than people who immediatly come up with the term fragile tests. Commented Jun 17, 2015 at 13:21
  • 6
    Guys, just because all these questions have the word "unit test" in the title doesn't mean they are duplicates. This question is coming at the problem from a different direction and is not an exact duplicate.
    – durron597
    Commented Jun 17, 2015 at 13:21
  • 7
    Your colleague sounds like the kind of coder I try to avoid working with. If tests fail when you add new code, the first response should not be to change the tests. They're saying you just broke something - go fix it! Of course if your tests are written badly, they'll fail when you've changed something unrelated, in which case you need to learn to write better tests. If your tests are fine, nothing is broken and it's still happening, then your application isn't architected to be testable, and you should fix that. Whichever it is, just tweaking the tests will only hide the real problem. Commented Jun 17, 2015 at 15:05
  • 4
    @Izkata That's a different scenario. If the design has changed, then the requirements that your code has to fulfill have changed, and the tests for that part of the code are out of date. In that case, changing the test is the right thing to do, period - and that should be done before you start work on the code. What I was referring to (and how I interpreted the question) is when you add Feature B and find that the tests for Feature A (whose design has not changed) start failing. Tests shouldn't be adapted to make your code pass; your code should be changed until it passes the tests. Commented Jun 17, 2015 at 15:16

6 Answers 6

65

By making them test just the thing they're about, and not lots of unrelated properties that are true now but might change later.

Some examples from my experience. Often systems are supposed to send notifications to their users when certain things happen. With a proper test harness, it's easy to mock email messages and verify that they go out, get to the correct recipient and say what they're supposed to say. However, it's not a good idea to assert simply "When this even happens, this user receives that exact message text". Such a test would fail whenever the I18N texts are revised. It's much better to assert "The message contains the user's new password / The link to the announced resource / the user's name and a greeting", so that the test keeps working and only breaks when the routine does, in fact, not do its stated job.

Similarly, when you test auto-generated IDs for something, never assume that whatever value is generated at the moment will always be generated. Even when your test harness doesn't change, the implementation of that feature might change so that the outcome changes while still fulfilling its contract. Again, you don't want to assert "The first user receives ID AAA", but rather "The first user receives an ID composed of letters, and the second user receives an ID also composed of letters and distinct from the first one".

In general, beware of testing things that aren't in the contract for the thing you're testing. Understanding what is essentially true about the behaviour of a unit and what is only accidentally true is the key to writing minimal covering tests - and it is also extremely helpful for understanding the system and maintaining it successfully. This is one way in which test-driven development improves outcomes even when not catching bugs.

1
27

For software to be highly testable, it must be designed and implemented with testing in mind. This is complementary to Killian Foth's answer, where the software is designed according to a contract, and the test is performed to verify that contract.

"Designing code for testability" means one will have to learn e.g. mocks, stubs, etc. These are various ways to create abstractions and "seams" to allow freedom in substituting software parts.

(The word "seams" is borrowed from the book Working Effectively with Legacy Code by Michael Feathers. Another recommended book is xUnit Test Patterns: Refactoring Test Code by Gerard Meszaros, which is a heavy one but is totally worth it.)


A language learner may have to learn a lot of things at once. So, it is possible that "designing code for testability" will increase the workload of a learner, and initially slow down the learning progress.

The suggestion is to try both ways. When working on a team, make a good effort to design for testability (and other quality attributes), but each person should also have some solo practice time where the focus is on learning the language itself, or to get something done quickly (because occasionally such need arises), or just to get creative and experiment with something in a completely new, unproven way.


Finally, a programmer who has enough experience with programming and design-for-testability will come to view "broken tests" favorably.

When the test is broken because the implementation changes in some way, if this happens when the programmer does not anticipate this, this is an illuminating moment where the programmer has learned something. For example, the programmer might not anticipate that the code behavior has changed, but the test is telling otherwise. When this happens, the broken test has successfully averted a potential bug.

2
  • 2
    I think it's better to avoid mocks, stubs, etc. when beginning. They add a lot of boilerplate/refactoring/etc. for little apparent gain, which can lead to negative feelings. Once a dev's written the same setup/teardown/cleanup/logging code for the umpteenth time, their utility becomes clear and they can be seen as positive. Also, by motivating their use properly, it's clear when they're not appropriate (ie. everywhere except the edges).
    – Warbo
    Commented Jun 18, 2015 at 11:08
  • Exactly. If your software is no implemented with adaptability in mind, then get ready for wasting lots of time whenever you need to add a new feature, especially with unit tests. To the OP, I highly suggest reading the chapter on Unit testing in the following book: amazon.com/Adaptive-Code-via-principles-Developer-ebook/dp/…
    – Eternal21
    Commented Jun 18, 2015 at 20:32
11

Yes, he's right - writing unit tests does increase your workload, and as you change your code the tests start to fail and you have to update them and that increases your workload even further.

However... that applies when you are changing code that is affected by the tests, typically you write code pieces that are tested by some unit tests and then you do not edit those piece of code again. After all, they work, and you designed them right so adding new features won;t require you to edit them, and I think this is where the problem lies. Inexperienced coders tend to hack code in without enough thought into the future of the codebase. You can add a class with a field, and then suddenly find you need to add another, and the original field needs to change type and so on, this churn is something you learn to avoid by 'getting it right' first time by thinking about what the code is supposed to do and how it will fit in to the product overall, not just solving the immediate problem at hand. Sure, we still need to make such changes, but it happens so much rarely with experienced devs.

One thing that you might find helpful in the intermediate time is using red-green-refactor style of unit testing. This is similar to using a debugger to test, in that you write code then run it in the debugger to see what errors there are, fixing them and repeating the debugging. Only instead of using the debugger, you use an automated test instead. Write the test, run it on the code, update the code and so on. The trick is to write little bits of code and test them very often and your unit tests become more like mini test harnesses. I find this approach fits better as its very much (conceptually) like the manual testing you're probably doing anyway.

6

one of my newbie co-workers says that tests make things more difficult.

I think the key here is what things? There are lots of processes involved in software development. Writing executable code is only one of them.

If we focus on writing executable(/compilable/etc.) code, then writing tests does make that more difficult, simply because there's more code to look after. For example, if we make a breaking-change like renaming a function, we will have to update the test code too, in order to achieve our goal of "executable code".

Of course, having executable code isn't the end goal: for a business the end goal is usually making money; for a developer the end goal is usually reducing software's buginess or increasing its feature set/usefulness, in order to support the business's money-making plan.

Software is so complex that it's rare to fix one bug or introduce one feature in isolation. Usually the changes we make introduce new bugs, or expose/worsen existing bugs. Adding new features can break old ones, either directly (they don't do what they used to) or conceptually (they don't make sense given the new feature).

The point of testing is to try and objectively keep track of "buginess" and feature sets. By having a test suite check that existing features work, we give ourselves some confidence that our work is increasing the feature set, rather than adding new things at the expense of old. By having a test suite check inputs/paths which are important, anticipated to cause problems (edge-cases), or have previously caused problems (regression tests), we give ourselves some confidence that "buginess" is decreasing, rather than fixing new bugs at the expense of old.

Testing follows a common pattern in software engineering: testing is claimed to make software better/more "agile"/less buggy/etc., but it's not really the testing which does this. Rather, good developers make software better/more "agile"/less buggy/etc. and testing is something that good developers tend to do.

In other words, performing some ritual like unit testing for its own sake will not make your code better. Yet understanding why many people do unit testing will make you a better developer, and being a better developer will make your code better, whether it has unit tests or not.

Unfortunately, we can't git pull these skills from each other. It takes time and effort, and each person must learn these things for themselves. It sounds like your colleague was forced into ritualistic testing without building up the understanding about why particular things are done.

I would recommend playing with these ideas in some toy projects. For example, if you see some explanation like "dependency injection is needed for situations like..." I would say try to make a toy example of that situation, see what problems you encounter, try to solve them, and then try to see what dependency injection is all about. That way, you will know when something is appropriate, you'll know when it's not, and you'll be able to explain and discuss different approaches to a problem (without just reciting what other people are claiming).

3

He argued that tests were constantly in his way when he tried to implement a new feature. The tests would fail after he had changed the code. So he had to adapt the tests which of course increased his workload.

That is the number one way in which tests help implement new features.

The tests would fail after he had changed the code.

The code sucked. Possibly the test code sucked. More likely his code sucked. Either way, the code sucked.

Let's say that there's about a 95% chance that it was his code that sucked, 2% chance that the test code sucks on purpose ("we have to disallow case A because while it's generally perfectly fine it causes problems with strange legacy client-setup B so we're testing to make sure nobody hits it") and 3% chance that the test code sucks.

Now that's being very generous, but even if the rates were up to 99.99% this wouldn't say anything bad about that person's skill or craftsmanship. (Consider how often you hit a compiler error; that's code that sucked too bad to even run the tests, but your overall work that day may have been fantastic, as may the final product when you fixed the source of those compiler errors). Indeed, its not really generous to hit any percentages here, test code tends to be the code to blame less often just because it's changed less often so it has fewer chances to acquire new bugs. And remember we're talking about the case of an identified failure, not about how often these failures happen.

Anyway, with those percentages there is a 98% chance that something is incorrect that needs to be fixed, a 2% chance that something is incorrect outside of your control that needs to be catered for, and a 100% chance that you need to change something.

The 3% chance that the test is incorrect further break down into a few possibilities:

  1. The test is buggy.
  2. The test is no longer relevant.
  3. The test is invalid for a particular case and is being applied generally.

As such, even when the test is to blame, it may not necessarily be "wrong" so much as out of date, some of the time.

In those 3% of cases, dealing with tests has cost you extra work that you wouldn't have had if you didn't use tests. But once that work is done it has added assistance to the 97% of the time when something else is to blame.

And in the 97% of times the test has led you to do more work right then but the result is that the non-test code doesn't suck any more (the 95% of the time) or deals with the external out-of-your-control issue fine (the 2% of the time). Note that the 2% not-your-fault case is still perceived by the user as "this software sucks".

So, 97% (realistically, more than that) of the time the test has made the code suck less.

Let's consider the alternatives for that 97% without the unit tests finding the flaws:

  1. The code's problems were still there, but nothing ever hit the relevant code path in real life. (Hey, it happens).
  2. The code's problems were found when other code wouldn't work with it.
  3. The code's problems were found when another developer had to do something with it.
  4. The code's problems were found in feature testing or application testing.
  5. The code's problems were found in user-acceptance testing.
  6. The code's problems were found after the product had been released, by annoyed users.
  7. The code's problems were found after the product had been released, by delighted crackers who then made use of it to compromise your or your customers' security.
  8. The code's problems weren't found, but did cause incorrect financial transactions, injury, loss of life, or continuance of enemy life. (Those last few are only applicable to increasingly specialised applications and may not apply to you, but they certainly do apply to some programmers).

Any professional programmer, and any amateur who releases their work, for that matter, will have experienced the first 6 of those cases happening. Some of us have experienced the final 2. They are the scenarios we most try to avoid, but they happen.

When items 2 through 5 happen, the impact on our workload is much greater than that of a failed unit test. Very often more time is spent dealing with these issues than developing. A great many projects have never gotten out of alpha because they never got past such issues.

For the later items in the list, the impact on workload may not be the worse thing about them.

The vast, vast majority of the time you have a failed unit test, you have been saved from one of the above scenarios. Unit testing won't save you from all of them, but it will save you from many of them.

Tests that pass are nice. They can give you a degree of confidence that at least certain bugs are not present in certain units, and that's great.

Tests that fail are the tests that have earned their keep.

One thing to note though:

The tests would fail after he had changed the code.

I hope there were also tests that failed before he had changed the code. All the tests related to the new feature should have started out failing, since the new feature wasn't implemented yet.

-4

Hey i am a new programmer too and i work at a company that has many many different projects, some use unit tests and some don't. My personal expereince urges me to tell you to use them. But don't worry, you get to choose the impact it has on your work load.

You can setup your unit tests in 2 ways (probably more). The first way is to have your unit tests plugin to you code directly. For example your project has a method that accepts 2 variables and outputs another. You can create test data and have your unit test call the method. This method is good if you want to ensure that your code is working correctly. However if you changed that method in your code without updating the unit test then, of course your unit test will fail.

Another way of doing it that does not impact your code at all, is by by simply copying and pasting in all the code you want to test in a unit test and creating all the faux data to go along with it. This will not alert you in anyway if your code is failing in your project, but it does give you a way to test your methods and make sure they work in the context of your unit test.

5
  • 3
    Your second approach sounds like a bad idea. Tests only exist to check the project's code. Having a green test suite on a broken project is worse than useless: it gives false information. It's much better to make the tests call the real project code, even if they all fail. The goal is not to have a green test suite, it's to have a working project.
    – Warbo
    Commented Jun 18, 2015 at 15:33
  • 1
    I have to agree with Warbo here. Tests are only because of what they tell you about the state of your main code base. If your tests are using different code then what exactly are they testing? How is this useful for your project?
    – shuttle87
    Commented Jun 18, 2015 at 15:44
  • 1
    @Warbo: The second approach is suitable as a temporary, throwaway test harness for use during prototyping, much like a scaffolding does when constructing a building. This alone does not invalidate the need for having a sound architecture and a long-lasting unit test suite. TDD is suitable if one knows what to assert next; but sometimes there is a stalemate where the only way out is write some (throwaway) code and see what it does. I speak this from algorithm research perspective - too many people think algorithm means copying from a textbook; it's not.
    – rwong
    Commented Jun 18, 2015 at 18:12
  • 1
    @rwong indeed, I wrote something similar but removed it to make my comment fit fit the limit. That kind of copy-paste is often done in documentation too, but in the context of the question that would add even more of a maintenance burden :) Also, it's perfectly possible to do testing without TDD, eg. adding in tests retrospectively. It's all a matter of cost/benefit tradeoff; the key is to try and understand those costs and benefits, rather than ignore potential solutions or follow doctrines religiously.
    – Warbo
    Commented Jun 18, 2015 at 19:55
  • 3
    That second way isn't unit testing at all, because it's not testing a unit. It's idea testing. Idea-testing ("hey, if I try this, will it work") can be very useful, but it's testing an idea, not a unit or any other produced software.
    – Jon Hanna
    Commented Jun 19, 2015 at 11:08

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