Isolating legacy code with ArchUnit tests

Clear boundaries in code are important ... and hard. ArchUnit allows you to capture the structure your team agreed on in tests.

Clear boundaries in code are important... and hard. Especially in legacy code. ArchUnit allows you to capture the structure your team agreed on in tests.

Starting to work with legacy code

Recently we picked up a project and attempted to move it forwards. We wanted to introduce new concepts and structures in a new UI. Also, we wanted to keep the old UI until the new one was ready for production. 
The new UI would start as a read-only version. It should not depend on existing code, allowing us to learn the codebase over time. Only in the next phase would we change the existing business logic.

Typically, you would document decisions like this, for instance in an ADR. This is a great way to capture the reasons behind the decision. But expressing the decision in tests gives additional benefits. 

The benefit of tests

The tests remind everyone on the team about the decision. If my code for the new UI depends on legacy code, the tests break. This obviously helps new team members learn about decisions that have been made. 

It is also helpful for those who have been on the team from the beginning. If in the heat of the moment a dependency sneaks in, then there is a failing test. Gently reminding you of the team agreement. Also, when you have bigger changes, maybe a refactoring, it's hard to keep track of all the changes. ArchUnit tests lift some mental burden off from you because they check that you agreed to avoid dependencies to legacy code. 

Failing tests as a chance

When a test fails, it gives you the chance to rethink the approach you just went with. Either you come up with a better idea, or you start a fruitful discussion with your teammates. Of course, the outcome of such a discussion can be to change or relax the rules a bit. But then it is a deliberate decision. Nothing sneaked in without you being aware of it. Changing the ArchUnit tests is something that can be easily seen in a PR review. Which again is a chance for a fruitful discussion with your team. 

The power of arch unit tests

ArchUnit tests run pretty fast. They won't slow your test suite down, giving you an additional safety net on every test run. This allows you to avoid dead ends when implementing or refactoring early; way earlier than a fellow developer can in a code review. 

So in short ArchUnit tests will help your team to

  • learn about decisions

  • stick to those decisions

  • trigger discussions about corner cases

And all of that with every test run. Which means early and reliable feedback.

Show, don’t tell

I hope by now that you are excited about ArchUnit. So what does this look like in code?
ArchUnit tests are regular JUnit tests. Just include ArchUnit as a dependency and you are ready to go.

// gradle 
dependencies {   
  testImplementation 'com.tngtech.archunit:archunit-junit5:0.20.1' 
}

In our project we decided that all code for the new UI lives in a package called v2. In the test we declare that this v2 package should not rely on legacy code. For this we use ArchUnits ability to assert on package dependencies. In the simplest form this looks as follows

@Test
public void version2PackagesShouldNotDependOnLegacyPackages() {
    // first declare the rule
    var isolatedVersion2 = noClasses()
            .that()
            .resideInAPackage("com.acme.webapp.v2..") // the trailing dot tells ArchUnit to match all sub-packages as well
            .should()
            .dependOnClassesThat().resideInAPackage("com.acme.webapp.legacy..");

    // second define on which classes the rule should be applied
    var webapp = new ClassFileImporter()
            .withImportOption(new ImportOption.DoNotIncludeTests())
            .importPackages("com.acme.webapp");
            
    // finally run the check
    isolatedVersion2.check(webapp);
}

But we cannot completely isolate from legacy code. We do want to make use of logged in users for example. So we need to allow some classes. For that we can create a custom predicate that defines the exceptions.

DescribedPredicate legacyPackagesWithAcceptedDependencies =
    new DescribedPredicate<>("reside in legacy package"){
        String[] acceptedDependencies = { "OrganizationDto", "IdentityContext" };

        @Override
        public boolean apply(JavaClass input) {
            var isAnAcceptedDependency = Arrays
                    .stream(acceptedDependencies)
                    .anyMatch(acceptedDependency -> input.getSimpleName().equals(acceptedDependency));

            return !isAnAcceptedDependency && input.getPackageName().startsWith("com.acme.webapp.legacy");
        }
    };

When put together the whole code looks like this

 package architecture;

  import com.tngtech.archunit.base.DescribedPredicate;
  import com.tngtech.archunit.core.domain.JavaClass;
  import com.tngtech.archunit.core.importer.ClassFileImporter;
  import com.tngtech.archunit.core.importer.ImportOption;
  import org.junit.jupiter.api.Test;

  import java.util.Arrays;

  import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

  public class LegacyIsolationTests {
      DescribedPredicate legacyPackagesWithAcceptedDependencies =
              new DescribedPredicate<>("reside in legacy package"){
                  String[] acceptedDependencies = { "OrganizationDto", "IdentityContext" };

                  @Override
                  public boolean apply(JavaClass input) {
                      var isAnAcceptedDependency = Arrays
                              .stream(acceptedDependencies)
                              .anyMatch(acceptedDependency -> input.getSimpleName().equals(acceptedDependency));

                      return !isAnAcceptedDependency && input.getPackageName().startsWith("com.acme.webapp.legacy");
                  }
              };

      @Test
      public void version2PackagesShouldNotDependOnLegacyPackages() {

          var isolatedVersion2 = noClasses()
                  .that()
                  .resideInAPackage("com.acme.webapp.v2..")
                  .should()
                  .dependOnClassesThat(legacyPackagesWithAcceptedDependencies);

          var webapp = new ClassFileImporter()
                  .withImportOption(new ImportOption.DoNotIncludeTests())
                  .importPackages("com.acme.webapp");

          isolatedVersion2.check(webapp);
      }
  }

Conclusion

What I like about putting team agreements into code like this is that it makes sure that the agreement is actually documented and followed. For new team members, it's easy to discover these rules; the tests will tell them. And for those who are around for longer time it's a nice nudge to think more closely about structure and dependencies. Or start a discussion instead of just being lazy and sneak in just that one exception to the rule.

Kompetenz 8/14/24

Legacy modernisation with eXplain

The tool for analysing code on the IBM i (AS400) & IBM Z (mainframe).

Mockup eXplain Codeanalyse Whitepaper
Whitepaper 7/2/24

eXplain - Download code analysis whitepaper

eXplain - The tool for code analysis on the IBM i (AS400) & IBM Z (mainframe)

Headerbild zu Cloud Pak for Data – Test-Drive
Technologie

IBM Cloud Pak for Data – Test-Drive

By making our comprehensive demo and customer data platform available, we want to offer these customers a way to get a very quick and pragmatic impression of the technology with their data.

Leistung 9/22/20

Working with CLOUDPILOTS

With the perfect partner at your side, many things are easier and more efficient, although a direct relationship with the manufacturer is appealing at first glance. But only at first glance!

Service

API Economy, DevOps, Low Code & MACH

Customer-oriented solutions on the topics of API economy, DevOps, low code and MACH (microservices, API-first, cloud-native and headless architecture)

Blog 9/27/22

Creating solutions and projects in VS code

In this post we are going to create a new Solution containing an F# console project and a test project using the dotnet CLI in Visual Studio Code.

Headerbild zu Automation mit Open Source
Technologie

Automation with Open Source

Automation tools provide a remedy by independently taking over some of the tasks that would otherwise fall to developers.

Unternehmen

Why work with us?

We live in the age of the customer. Changes due to digitalization and integration have placed the focus even more on the customer. Customers have never been this important, and they are more powerful market players than ever before.

Headerbild zu Containerisierung mit Open Source
Technologie

Containerisation with Open Source

Containerization is the next stage of virtualization and provides secure and easy compartmentalization of individual applications. The process of deploying an app has been simplified many times in recent years.

Headerbild zu Datenbanken mit Open Source
Technologie 11/12/20

Databases with Open Source

Every dynamic application needs some form of database to store its data logically and sorted. However, there is no one-size-fits-all solution, but it should always be looked at the use case to make the appropriate choice.

Kompetenz 8/5/21

Shaping the future with technology

ARS Computer and Consulting is one of the leading companies in the field of software engineering. Our mission: The Art of Software Engineering. This includes high-quality consulting and successful projects for the agile development of high-quality software.

Blog

Expanding Opportunities with Generative AI

Discover how nonprofits use generative AI to boost career opportunities, enhance education, and bridge employment gaps for underserved communities.

Headerbild zu Webserver mit Open Source
Technologie 11/12/20

Web server with Open Source

Web servers provide their application with the gateway to the world: this is where requests for data for a complex web app and resources for a website go in and out.

Kompetenz 10/19/22

Digital Transformation with Atlassian Tools

At catworkx, we digitalize business processes for our customers on the basis of Atlassian tools such as Jira and CoAnd you can also benefit from the flexibility, performance and transparency of Altlas

Blog 7/14/23

Event Sourcing with Apache Kafka

For a long time, there was a consensus that Kafka and Event Sourcing are not compatible with each other. So it might look like there is no way of working with Event Sourcing. But there is if certain requirements are met.

Blog 3/11/25

Answering Business Questions with LLMs

8th place in Enterprise RAG Challenge 2025: Answering Business Questions with LLMs

Felss Logo
Referenz

Quality scoring with predictive analytics models

Felss Systems GmbH relies on a specially developed predictive analytics method from X-INTEGRATE. With predictive scoring and automation, the efficiency of industrial machinery is significantly increased.

Referenz

Revamped Idea Management with numerous Enhancements

In 2011, the management decided to reorganize the idea management system and give it a complete overhaul. The existing software solution was no longer up to date and had to be replaced.

Referenz

Smarter mobility with the portal switchh

Subway, S-Bahn, bus, car, ferry or bicycle: The pilot project "switchh" of HOCHBAHN in cooperation with Europcar and Car2Go makes Hamburg mobile.

Referenz

Agile working with Scrum at Wienerberger

In 2019, Wienerberger introduced agile methods with Jira software. With catworkx and a 4-phase plan, requirements were clearly defined, transparency increased and development quality improved.

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!