Legacy Doesn’t Always Bring Good!

Beykan Şen
Trendyol Tech
Published in
8 min readDec 16, 2021

--

From a software development perspective, the word “legacy“ often connotes something bad. But according to the dictionary, legacy means that “an amount of money or property left to someone in a will”. It seems like a good thing, right?

Let’s take this analogy; your deceased grandfather left you a shop that floating in debt. You can’t sell it because it's mortgaged. You can’t run it either because you don’t have enough money to pay debts and then make some profit. So in this situation, it's more harm than good to you.

Unfortunately, in the software development world, most of the legacies are similar to your grandfather’s shop that you’ve inherited. In my opinion; legacy code does not mean old. It means codebases that lack any or proper tests and are hard to read and understand, hard to change. In other words, hard to maintain like the shop that you’ve inherited from your grandfather.

According to my way of thinking, one of the most important reasons why it is unmaintainable is the lack of unit testing. In other words; without proper dependency management and separation of concerns, without confidence about changes, without live documentation, we wouldn’t expect anything else other than that. Because, if you approach with a unit test mindset while writing code, you will have proper dependency management, separation of concerns and always up-to-date documentation for your code without any extra effort. Also, some of the SOLID principles comes free. So you can easily add new features or change existing ones and you will be more confident about these changes. Also, I suggest you try TDD so the unit test mindset comes free too.

I think all of us agree on the importance of testing in software development. If I return to the main topic of this article; becoming a legacy is not a one-way street. If you have codebases that have become legacy, before considering re-platforming, maybe you should start to write unit tests to rescue it. I am not saying that lack of unit tests is the only reason for this but it’s one of the most important ones. If you think about how to write unit tests in the legacy codebase, the answer is “break the dependencies”.

Break The Dependencies

In this part, I will try to summarize some techniques that I’ve used the most while breaking dependencies. Since dependency breaking has become easier thanks to mocking and testing libraries, which are widely used today, I did not find it necessary to include all known/written techniques in this post.

However, in these techniques I have included, I tried not to include any language, library or framework-dependent code to explain its logic as pure OOP. For example, in Java, you can use Mockito to mock concrete implementation of the class under the test. So maybe you don’t need to extract an interface from it.

Also, these techniques are deeply explained Working Effectively with Legacy Code book by Michael Feathers. For other techniques that I haven’t included in this post, you should get a copy of this book.

1- Extract Interface

Your code should depend on abstractions, not implementations. If your class depend on abstractions you can mock them or make fake implementation in your testing suite.

For example, UserService depends on UserRepository. While testing register() method, you should not hit the real UserRepository because it may call a database and you will have unexpected results.

Extract Interface Before

So you should make these changes to make UserService testable.

  • Extract UserRepository to a new interface.
  • Change UserService’s repository dependency with that interface.

New UserService should look like this:

Extract Interface After

and test case may look like this:

Extract Interface Tests

As I mentioned at the beginning of this part, I didn’t include any framework or library dependent code in these examples. But if you use any mocking libraries, you don’t have to write your own UserRepository fake implementation and/or extract an interface from it. Also, you can verify the method call on the mocked object. So you wouldn’t need to handle userHasSaved state on FakeUserRepository just for assertions.

2- Subclass and Override Method

The idea of this technique is that you can use inheritance to change the behaviour of dependency under the test.

It's very similar to “Extract Interface” technique. The only difference is you don’t need to extract an interface from an existing class. You only need FakeUserRepository implementation as a subclass of UserRepository that overrides save() method.

With the same example in “Extract Interface” and according to this technique, modified FakeUserRepository might look like this:

Subclass and Override Method Tests

If your programming language of choice doesn’t support overriding as default behaviour, you should make your class and/or method overridable.

3- Adapt Parameter

Sometimes you are not able to use the “Extract Interface” technique because concrete dependency is too large or you are not able to use the “Subclass and Override Method” because you don’t have control over the class. (external jar, DLL etc.)

In this situation, you can use the Adapt Parameter technique to create an adapter for just methods you care of.

Let’s consider this example; you have CamRecorder class that has 20 or more methods. You also have FootageService class and one of its methods depends on CamRecorder and gets the latest recorded footage video path from the local file system.

How can you write a test to uploadToCloud() method?

Adapt Parameter Before

Using “Extract Interface” may not be wise because CamRecorder class is too large and using “Subclass and Override Method” may not be possible because it’s in an external library you don’t have control over it. So, for making uploadToCloud method testable, you should break CamRecorder dependency via the Adapt Parameter technique.

Firstly create a new class and interface for adapting CamRecorder. Let’s call it CamRecorderAdapterImpl and CamRecorderAdapter. In CamRecorderAdapterImpl accept CamRecorder as a constructor parameter. Create getLatestFootageFilePath method with the same signature as the original CamRecorder class. Call original class’s method in this newly created method. Change uploadToCloud method parameter type with CamRecorderAdapter.

Adapt Parameter After

Create Fake CamRecorderAdapterImpl for testing. Unit tests of FootageService may look like this:

Adapt Parameter Tests

4- Breaking Static/Global References

When you are writing a test to the class that has static dependencies, you’ve more than one choice that you can choose.

  • You can use some mocking libraries that provide changing behaviour of statics under test.

This is the easiest one. For Java, you can follow this guide if you use Mockito. But for the purpose of writing this article as I mentioned above, I didn’t find it useful to share an example for this language-specific solution.

  • If you own that class, you can provide a setter.

I do not recommend it. Please avoid using mutable global data. So I won’t give any code example about this.

  • Write a wrapper for a static method that you can fake.

Let’s take this example; you have calculatePromotions() method in Cart class that has some static dependency. This method sets free shipping If totalPrice bigger than MinBasketPriceForFreeShipping.

How can you test this method’s if branch in this situation? Of course, you can directly modify totalPrice of Cart or modify items to meet the minimum price, but you get the idea and remember, we are trying to emulate legacy code. In real life, you might come across much worse than this :).

Break Static References Before

You can write a wrapper for PromotionMinimumPrices as PromotionMinimumPricesWrapper and Change calculatePromotions()’s static dependency with this wrapper class.

Break Static References After

Unit test of Client class may look like this:

Break Static References Tests

Write a faker sub-class of PromotionMinimumPricesWrapper PromotionMinimumPricesWrapper and set fakeMinBasketPriceForFreeShippingvalue according to the test case.

  • Introduce Instance Delegator

The idea behind this, create an instance method that wraps the static field or method and you can fake it under test.

Introduce Instance Delegator After

It’s very similar to writing a separate wrapper for PromotionMinimumPrices but in my opinion, it’s more cleaner.

5- Expose Static Method

Sometimes you can’t initialize class in the test suite because of constructor dependencies. If the method that you try to test doesn’t use instance data or methods, you can turn it into a static method. When it is static, you can get it under test without having to instantiate the class. I rarely use this technique but it might be beneficial to keep in your toolset.

In this example, we have MailService that have some dependency on its constructor and we try to write a test for validate method.

While writing the unit test for validate we have to initialize MailService class but before then we have to create a fake Session to pass the constructor of MailService. For this code sample, you think to create a fake Session object because it wouldn’t be much of a problem since there was already one. but consider if MailService require 5 objects to initialize. Remember it’s legacy code :)

Expose Static Method Before

If we look closely, validate(String emailAddress) method doesn’t use any instance data or methods. If we create a new static method and copy the body to it, we can easily test it. In addition, we can call the newly created static method from validate(String emailAddress) so clients of this class will not be affected.

So MailService and MailServiceTest may look like this:

Expose Static Method After & Tests

Also, you can modify validateEmailAddress access modifier to protected or package-private to (or any level in your programming language to only be visible to your test) prevent the use of clients. But for Java, your tester class should reside in the same package as your tested class.

These techniques are among the ones I use the most. Actually, while writing code samples about these techniques I aimed to cover some assistive techniques too like Parameterize Constructor, Parameterize Method.

I hope you find it useful. Happy coding :)

--

--