Skip to content
All posts
February 24, 20264 min read

Why Is Your Test Automation So Slow? (And How to Fix It)

A test suite that takes 45 minutes to run doesn't get run. Slow automation defeats its own purpose. Here are the real causes of slow test suites and concrete fixes for each one.

TestingAutomationPerformanceCI/CD
Share:

A test suite that takes 45 minutes to run will eventually stop being run before every merge. Engineers start skipping it. The CI step becomes optional. Slow automation is failed automation.

Here are the actual causes — and fixes.


Cause 1: Sequential Execution

The default for most test runners is sequential: run test 1, then test 2, then test 3. For a suite of 500 tests averaging 2 seconds each, that's over 16 minutes — from sequential execution alone.

Fix: Parallelize

Most modern test runners support parallel execution. In JUnit 5:

kotlin
// junit-platform.properties
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.config.strategy=dynamic

In Gradle:

kotlin
tasks.withType<Test> {
    maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
}

[!WARNING] Parallel tests must be completely isolated — no shared state, no shared database rows, no shared files. Parallelizing tests with shared state introduces flakiness.


Cause 2: Too Many End-to-End Tests

UI/E2E tests are 10-60x slower than unit tests. A suite dominated by Espresso or Selenium tests will always be slow.

Fix: Move logic down the stack

Before writing a UI test, ask: can this be tested at the unit or API level?

  • A business rule validation → unit test
  • An API response format → API test
  • A navigation flow → UI test (justified)
  • Whether a button is disabled when form is empty → UI test (justified)

Most coverage of application logic doesn't require a browser or device.


Cause 3: Slow Test Setup and Teardown

If every test restarts a database, clears all data, seeds fresh fixtures, and then runs a single assertion, the test itself is a tiny fraction of the total runtime.

Fix: Share expensive setup, isolate cheap teardown

kotlin
companion object {
    @JvmStatic
    @BeforeAll
    fun setupDatabase() {
        // Expensive: runs once for the test class
        testDatabase = startTestDatabase()
        seedBaseData(testDatabase)
    }
}

@BeforeEach
fun resetMutatedData() {
    // Cheap: only reset what this test changes
    testDatabase.rollback(lastTransaction)
}

Use

code
@BeforeAll
for expensive setup that can be shared,
code
@BeforeEach
only for what must be isolated per test.


Cause 4: Real HTTP Calls in Tests

Tests that call real external services — payment gateways, email providers, third-party APIs — are slow by definition. Network latency, rate limits, and external availability all affect them.

Fix: Mock external dependencies

kotlin
// Instead of calling the real payment API
val mockPaymentGateway = mockk<PaymentGateway>()
every { mockPaymentGateway.charge(any()) } returns PaymentResult.Success(transactionId = "test-123")

val orderService = OrderService(paymentGateway = mockPaymentGateway)
orderService.placeOrder(testOrder)

verify { mockPaymentGateway.charge(testOrder.total) }

Mocked HTTP calls take microseconds. Real ones take 100ms–2s each.


Cause 5: No Test Result Caching

Running 500 tests when only 3 files changed is wasteful. Gradle's build cache and test caching mean unchanged tests don't re-run.

Fix: Enable Gradle build cache

kotlin
// gradle.properties
org.gradle.caching=true
org.gradle.configuration-cache=true

Tests in unchanged modules are loaded from cache. In large projects, this can reduce test time by 80%+.


Cause 6: Tests That Wait for Time

kotlin
// Waiting for an email to be "sent" by sleeping
Thread.sleep(3000) // hope the email sends in 3 seconds
assertEmailReceived(address)

Arbitrary sleeps are slow and unreliable. They're often set to the worst-case time, not the average time.

Fix: Use event-driven waits

kotlin
// Poll until condition is true, up to a timeout
awaitUntil(timeout = 5.seconds, interval = 100.milliseconds) {
    emailService.hasReceived(address)
}

Poll frequently with a reasonable timeout instead of sleeping for the worst case.


Cause 7: Bloated CI Runner Configuration

Each test job spinning up a fresh Docker container, pulling dependencies, warming up the JVM, and running a gradle daemon adds minutes of overhead.

Fix: Cache dependencies in CI

yaml
# GitHub Actions example
- uses: actions/cache@v3
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ hashFiles('**/*.gradle*') }}

Dependency caching saves 2-5 minutes per CI run. On a team with 20 PRs/day, that's significant.


Benchmarking Your Suite

Before optimizing, measure:

bash
# Run the suite and measure time by module/class
./gradlew test --profile
# Opens an HTML report with timing breakdown

Find the 20% of tests taking 80% of the time. Fix those first.


Targets Worth Aiming For

Suite SizeTarget Run Time
< 100 tests< 30 seconds
100–500 tests< 2 minutes
500–2000 tests< 5 minutes
2000+ tests< 10 minutes with parallelism

If your CI takes longer than these targets, it's actively slowing development. Engineers start batching commits to avoid the wait, which defeats the purpose of CI.


Takeaways

  • Parallel execution is the highest-ROI fix for slow suites
  • Too many UI/E2E tests is the root cause for most chronically slow suites
  • Mock external dependencies — real HTTP calls kill performance
  • Cache Gradle/Maven dependencies in CI — this is often overlooked
  • Measure before optimizing — find the slowest 20% and start there
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus