Code written without tests is legacy code. There is no such thing as legacy code, only legacy developers. The past is present, and the future will be here soon enough.
Code is only legacy if it’s not covered by tests. If you have tests, you can refactor. If you don’t have tests, you can’t change it. We have to keep our code under test to keep our refactoring costs down. If we don’t do that, we are going to live in the past forever because we’re too afraid to face the future.
How do we keep our code under test? It’s not a question of “if” but “how.” How do we keep it under test? We have to design our systems so they are easy to keep under test. We have to “embrace change” in order to refactor constantly or else we’ll just get stuck forever living in the past.
The key to keeping a code base under test is to keep it modular. The reason is simple: if your code calls lots of methods in different classes and packages, you need to write tests that exercise all of those call paths. If your code isn’t modular, you’ll be testing the same behavior multiple times through different call paths, and testing the same parts of your code more than once.
When you can’t change the public interface of your class, you’ll need to use refactoring techniques like Extract Class and Extract Interface when some functionality changes. This will likely result in having a set of small classes which implement one aspect of functionality each. When this happens, it’s easy to keep the code under test because each class usually only needs a few tests. When your classes are large, not only do they implement multiple aspects of functionality, but they also have many internal dependencies which need to be tested as well.
A few years back, I was working on a project with a code base that was pretty old. This code base was in use for about five years and had gone through several developers. One of the problems with this software was that it wasn’t under test. The previous developers knew that writing tests was important but they never got around to it. Once I started working on this code base, I was faced with the daunting task of adding tests to legacy code.
I had to start testing legacy code by using techniques like dependency injection and mock objects which are hard to understand at first. After spending several months on this project, I finally found a way to keep my code under test without having to do any big refactorings. I followed the three rules of TDD and never wrote new production code unless I had a failing test, but at the same time didn’t want to spend a lot of time writing tests for everything all at once. So how did I do it?
Since the software system I worked on consisted of components (classes) that were highly cohesive and loosely coupled, I ended up writing integration tests rather than unit tests for each class individually. This way, when a bug was found in one component, I only needed to write one test for that component rather
It’s a new year and you’re jumping into a new project. You’ve been warned that it’s a big ball of mud and that the existing code base is riddled with bugs and has no tests. You have your work cut out for you!
The big question is: what kind of work should you do to get the code base into a better state? Should you spend all your time writing tests before making any changes to production code? Or should you dive right in and try to fix some bugs?
There are many approaches. In this article, I want to describe one way to keep a legacy code base under test while still getting things done. I call it “test-driven bug fixing.”
We all have it. Code that is old, crufty, and hard to understand. We’re reluctant to touch it for fear of breaking something, but we don’t know what will break if we do change it. We’ve all been there.
But there’s a way out of the mess! Test-driven development (TDD) can be used to help you understand the code and keep it doing what you expect as you clean it up. Done well, TDD can help you develop the courage to refactor legacy code into something better.
In this talk, I’ll go over how I used TDD to change some legacy code that had no tests at all and was nearly impossible to understand into something that I could easily refactor and extend. Along the way I’ll cover some common pitfalls in implementing tests on legacy code that I ran into, and how to overcome them.
I’ll also cover some tools that will help you along the way:
– Using a debugger to explore code that has no tests
– Creating a test harness for existing code
– The importance of documentation when writing tests
The first thing we need to do is understand what legacy code is and what the problems are.
Legacy code, by definition, is code without tests. It has no safety net. You can’t break it, because it’s already broken. If you add a test now, all you can do is prove that it’s broken in exactly the same way that it was before.
But there are many other ways in which legacy code is bad:**
* It’s usually not object-oriented or well-factored, so changes ripple through the whole system.**
* It’s usually not documented, so you will spend a lot of time and effort figuring out what it does and how.**
* It’s often hard to debug; it does not have good error messages or logging or auditing or debugging hooks.**
* It’s hard to use; it may be poorly factored and tightly coupled, so you’ll have to write lots of wrapper code to get at the bits you really want to use.**
* It’s hard to test; probably it was not written with testing in mind, so there are no hooks for tests, no seams for injecting dependencies.**
* And worst of all, legacy code almost always embodies
For most of us, our first contact with legacy code is when we inherit it. It’s that code base that worked in the past, but now it just sits there, making money at a constant rate, but thwarting any new changes.
What’s the best way to deal with legacy code?
1) Understand the system.
2) Write tests for the code.
3) Make gradual improvements.
4) When you check in, always keep the code working.
5) Add simple abstractions when you need them.