Skip to content
Smitheo

Efficient Testing for Better Results: Boosting Productivity and Reducing Development Costs

How we cut slow unit tests from 15 minutes to 1 minute on an AEM project, and how faster tests improve productivity and delivery.

By Matija Kovacek · Published

Last updated

Optimizing slow unit tests isn’t just about saving time. It’s about driving productivity and enhancing development quality.

Listen on Spotify.

In this article, we explore the practical steps taken to reduce test execution time from 15 minutes to just 1 minute. Learn how these optimizations can improve team workflows, minimize costly errors, and ensure faster project delivery. Whether you manage large-scale projects or want to refine your testing processes, these insights will empower your team to achieve more.

Understanding the motivation behind optimizing slow unit tests is crucial. We'll explore the challenges faced by the Client, why we wanted to fix them, and the good things that happened afterward. Expect insights into how faster tests can boost productivity and project success.

Why?

  • 90% reduced project build time
  • 11x faster project build speed
  • 13x faster unit test execution

Maven reactor summary before and after the optimization: total build time drops from 17:43 min to 01:38 min

Imagine how this affects a big development team where each team member builds the project several times per day.

Consequences:

  • Slower development
  • Developers locally skip test execution
  • Failing deployments to higher environments

Some Facts Why We Did It

  • We like unit/integration testing topics
  • We like to optimize things
  • We prefer Integration over Unit tests
  • Test Diamond > Test Pyramid
  • We don’t like mocks

Test diamond diagram: a few manual tests and E2E tests on top, many integration tests in the middle, critical unit tests at the bottom

Project

  • AEM Multi-tenant Project
  • Some tenants already live, and some are in development
  • Joined at late stage of the project
  • Code coverage around 60%
  • Unit/Integration tests are not in the best shape
  • 343 test classes
  • 1083 test methods

How to not write Unit Tests

  • Don’t write a test that doesn’t make sense
  • Don’t test if Mockito when() then() methods work correctly
  • Don’t overuse mocking
  • Don’t import and use not needed objects and their methods

Java test class SEOModelTest with many @Mock fields for JSON, page, locale, resolver and resource objects

Java @BeforeEach setUp method stubbing many mocked calls with Mockito when().thenReturn(), followed by a breadcrumb schema test

  • Don’t test directly if POJO getters and setters work correctly
  • Don’t write tests just to achieve numbers

Java ContentCardBeanTest that sets bean properties and asserts each getter returns the same value

  • Don’t call external endpoints in unit tests (mock their responses, e.g use Wiremock)

Java APIServiceImplTest with two contact tests that load request JSON and call the API service without asserting any result

Optimization Strategy

Since some projects are already live and multiple development teams are working with the same codebase, the optimization strategy was simple:

  • Only quick wins
  • Minimal code changes
  • Easy code changes
  • Don’t change test code logic
  • Don't change implementation code logic

1. Optimization: Parallel Test Execution for Junit 5

One of the first things that would come to everyone's minds is, let's introduce some concurrency and parallelism. This should speed up the test, but not solve the root cause, and this is just fine for our optimization strategy.

IntelliJ view of junit-platform.properties enabling JUnit Jupiter parallel execution in concurrent mode

What happened:

  • Some tests were failing due to not isolated test context between test cases
  • The test code change was required
  • Execution time didn’t decrease

Optimization

  • Failed

2. Optimization: Easy Code Changes

Optimization

  • Improved test speed execution from ~15 min to ~5 min
  • 3x faster unit test execution

Changes:

  • Removed not needed AemContext object and AemContextExtension

Code diff of ContentCardBeanTest showing removed unused imports, MockitoExtension and AemContextExtension

  • Removed not needed MockitoExtension and not needed Mockito init(), open() initialization methods

Code diff of BusinessRulesListenerTest removing AemContext and manual Mockito initialization in favor of MockitoExtension

  • Replaced @Mock annotations with mock(ClassName.class) method

Code diff of BlogListCardModelTest replacing @Mock annotations with mock(ClassName.class) calls and removing unneeded mocks

  • Removed not need mocked objects and service registrations

Code diff of BlogListCardModelTest moving context loading from @BeforeEach to a static @BeforeAll setup method

  • Converted @BeforeEach to @BeforeAll

Code diff of BlogListCardModelTest showing @BeforeEach converted to @BeforeAll and mock service registrations removed

  • Set appropriate ResourceResolverType in AemContext

Code diff of a backup test class choosing a simpler AemContext without an explicit ResourceResolverType

  • Mock endpoint calls and responses

WireMockUtil Java class with a mockExpectedPostResponse helper built on WireMock givenThat, post and aResponse

Code diff of APIServiceImplTest rewritten to use WireMock to mock endpoint calls and assert the responses

3. Optimization: Maven Surefire Plugin & Test Logging Library

Each test was gradually taking more time to execute, meaning that we had potential memory leaks and big CPU activity. We found out that the Test logging library is logging all logs (trace) in memory.

Maven pom.xml diff replacing the slf4j-test dependency with slf4j-simple and setting the log level to off in Surefire

Changes:

  • Replaced uk.org.lidalia:slf4j-test with org.slf4j:slf4j-simple
  • Turned off Test Logging

Optimization

  • Improved test speed execution from ~5 min to ~1:30 min
  • 3.3x faster unit test execution

4. Optimization: Maven Surefire Plugin & Forked Test Execution

Since the first try with concurrency and parallelism optimization failed, we were a bit stubborn and started investigating further so we found out that we could execute tests concurrently by configuring the Maven Surefire Plugin.

Maven Surefire plugin configuration with forkCount set to 2 and an argLine memory setting

The parameter forkCount defines the maximum number of JVM processes that maven-surefire-plugin will spawn concurrently to execute the tests.

Changes:

  • Set forkCount=2
  • Note: bigger values didn’t bring bigger optimization, potentially can cause build crash on the machine with low resources

Optimization

  • Improved test speed execution from ~1:30 min to ~1 min
  • 1.5x faster unit test execution

5. Bonus tip and final optimization: Maven Daemon

Long story short, Maven Deamon builds maven modules in parallel. If you want to read more about Speeding up the Maven Build time with Maven Daemon, check blog post Faster Builds in AEM: Save Time with Maven Daemon

Optimization

  • 90% reduced build time
  • 11x faster project build speed
  • 13x faster unit test execution

Maven reactor summary before and after the optimization: total build time drops from 17:43 min to 01:38 min

Conclusion

  • Remove uk.org.lidalia:slf4j-test and turn off or decrease the log level in maven-surefire-plugin: Reduction from ~15 min to ~ 2 min
  • Set forkCount in maven-surefire-plugin for Parallel Test execution: Reduction from ~15 min to ~ 6:40 min
  • Treat your tests and write them as an implementation code (clean, quality, performant): Reduction from ~15 min to ~ 5 min
  • Use additional tools to speed up the development process like Maven Daemon and aemsync

If you want to read more about Unit/Integration Testing, check blog post Testing Behavior, Not Implementation: Building Better Software Faster.

Listen on Spotify.


Related: Slow test suites are one of the first things we measure in an AEM audit.

Need help with something like this?

Tell us what you want to automate, fix or build.

Start a project