Back

TDD And Defying Legacy Code

TDD And Defying Legacy Code

image alt text

Some software pieces can be harder to maintain than others, Why? and how using TDD can help software engineers avoid legacy code problems?

Almost at any point in your software history, there is this dark area, the complicated piece of software. It has been there on production for a long time.

It’s stable to some extent, valuable and generating money but written in a bad pattern or old technology. You are dying to change, but you are too scared you may damage anything, that’s what we usually call “Legacy” code. It can be even worse if it lacks unit tests and documentation. Imagine your product requires complex behavior, with no straight forward implementation. And the only way to fully know how it behaves is to manually test your product. Or maybe the code has some tests but the tests are very bad and not covering full scenarios or mislead you.

image alt text

So we can define legacy code as:

  • any complex code without tests*.*
  • any code with bad tests.

How does Legacy Code happen?

According to one of Lehmans Laws:

As a system evolves, its complexity increases unless work is done to reduce it.

And in the world of continuous rising startups, everything is about how fast you can deliver. You only care about building an MVP before your competitors.

Building more features and invading green markets. which need continuous software changes resulting in increasing complexity.

Legacy code is the result of many people working on the same project, over a long period of time, with conflicting requirements, under time pressure.

Legacy code has no end

image alt text

If your product requires a lot of changes, if you push new features every day then you’ll always have legacy code.

And it’s okay for the product requirements to drive the development, which is the case most of the time.

You need to live with it, try to cut it whenever you could, and a good way to do that is by Continuous Refactoring.

Refactoring is improving code structure, design, and implementation. Which increases readability and reduces complexity, but without affecting the Product Behavior.

If you found yourself changing Product Behavior that’s called a product change and not Refactoring.

If you find yourself trying to Refactor old code while implementing new features (as a way to save time) please stop doing that!

Developers tend to make mistakes when working on many tasks at the same time. It’s usually better to just focus on either Adding new code for product change or Refactoring old code.

It sounds scary to edit legacy and it’s. that’s why we need a safety net to make sure whatever we change we are not changing any behavior. that’s exactly what automated unit tests should do for us.

Why Unit Tests?

Let me tell you a common story that happens at work, If there’s one thing we’re crazy about at Instabug, it’ll be Customer Happiness.

A client reports a problem, we jump to fix it ASAP, we identify the root cause for the problem and fix it.

What happens is I make a hotfix that I think would work for the client, So I make a pull request with the fix, merge it.

And we hurry to reply to the client that his problem is fixed, and yes it’s fixed, but there is a catch. A few minutes later we find the client comes back to us frustrated with a new issue!

Cause my hotfix worked for the problem but it, Unfortunately, ruined another part that I didn’t test. We end up rolling back the fix and trying something else.

So what went wrong here?

Yes, the problem was that we didn’t test all the parts that could be affected aka (legacy code). Mainly because it may be very time consuming if done manually. That’s where automated Unit Testing comes to be helpful.

Unit Testing: is some extra code separated from your application code, you write that code to test each component in your code as an isolated unit.

If only we had all app functions covered with tests, we would have caught the bug while running tests. A step that should be running before deploying to production, ensuring high-quality fixes.

All right, so let’s write unit tests, a good way to write tests is TDD.

What is TDD?

Test-Driven Development is a way of development, where you let unit testing drive and impact your development.

Usually, developers write tests during/after writing the code. TDD is about writing tests before the actual code, but how would you write tests for code that’s not there yet?

Inspired by the agile development, TDD works in iterations, A loop of repeated actions that developers do till they finish development. Here are the loop steps:

  • ☂️ write a test for the smallest function you want to implement.
  • ▶️ run the test
  • ❌ watch the test fails (remember you didn’t write any code yet 🤷‍♀️)
  • ✔️ write the smallest code change to make the test pass
  • 🔨 Refactor code
  • 🔁 repeat

Why TDD?

That way by the time you finish your feature/project you’ll also finish unit tests, here are some notes about TDD:

  • Increase productivity: Although TDD means writing tests which are extra code. which means spending more effort and time which seems like slowing your process. But on the long term TDD increase the chances of noticing bugs and code that need to refactor as early as possible which saves time in the long term.
  • High-quality code: It was found that TDD reduces bugs by 40–60%.
  • It's More Fun: TDD cycle is like a Reward cycle, It consists of (trigger - action - reward). First, you see your test fails which triggers you to take action and write code to fix it. Then you receive your reward seeing your tests pass. this's when your dopamine level increases, it just feels great, and you simply get hooked in the cycle.

Conclusion

Whether you’re refactoring legacy code or just fixing a bug and don’t want to mess everything up, TDD will increase code coverage which acts as a safety net for your Refactoring, More Confidence pressing that merge button without worrying about ruining other affected parts that you or your team members working on.

More on the topic

A good book on the topic Working Effectively with Legacy Code