Skip to Content
Mobile Automation
Sign in
  • Home
  • Start Here
  • About
  • Blog
  • Tags
  • Contact
  • X

Configuration Management for Mobile Tests, Part 2: Run Any Combination from CI

Run any platform, device & env combo from CI by name.

Featured Post For Members Framework June 23, 2026
Configuration Management for Mobile Tests, Part 2: Run Any Combination from CI
On this page
Unlock full content

In Part 1, you replaced the flat properties file with a typed DriverConfig object and split configuration into per-dimension profile files — a platform file, a device file, and your personal git-ignored local file. You can now switch platform and device purely by name:

mvn clean test -Dspring.profiles.active=android,pixel8,local-android

That's the foundation. This part extends the exact same pattern to three more dimensions — the backend your test code talks to, how a run behaves, and the secrets a cloud run needs — then wires the whole thing into CI so a pipeline picks any combination by name, with nothing in src/ changing between runs.

💡 This is Article 6 (Part 2) of the Framework Series. It continues directly from Part 1, which built the typed DriverConfig object and the platform/device/local profile files. Everything here assumes that structure is in place: the base application.properties with its spring.config.import line, config/platforms.yml, config/devices.yml, and your git-ignored local files.

Step 1: Add Environment Profiles

Before creating these files, it's worth being clear about what an environment profile actually does — because it's easy to misunderstand.

The environment profile does not configure what server your app talks to. That's baked into the app binary at build time. A staging .apk already points to the staging backend; a prod .ipa already points to prod. Your test framework can't change that.

What the environment profile configures is what server your test code talks to — directly, independently of the app. This matters when your tests need to make API calls themselves:

  • Test data setup — creating a user account via API before running a login test, rather than tapping through a registration flow every time
  • Test data teardown — deleting test records after a test to leave the environment clean
  • State verification — asserting that a UI action triggered the right backend change by checking the API response directly

If you're writing pure UI tests that only interact with the app and assert on screen state, you don't need this yet. The demo app used through this series has no real backend, so env.api-base-url has no consumer here. The pattern is introduced now so the configuration dimension exists when your tests grow to need it.

Following the dimension-file pattern, create config/environments.yml with one document per backend:

# Environment profiles: which backend your test code talks to
spring:
  config:
    activate:
      on-profile: staging
env:
  api-base-url: https://staging-api.example.com

---
spring:
  config:
    activate:
      on-profile: prod
env:
  api-base-url: https://api.example.com

(environments.yml is already in the spring.config.import line from Step 3 — no extra wiring.)

Bind these with the same typed-config pattern. Create core/src/main/java/com/mobileframework/config/EnvironmentConfig.java:

package com.mobileframework.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "env")
@Getter
@Setter
public class EnvironmentConfig {

    private String apiBaseUrl;

}

Now combine dimensions. Activate a device profile and an environment profile together:

mvn clean test -Dspring.profiles.active=ios,iphone17,local-ios,staging

Spring loads all the active documents. ios sets the platform and app; iphone17 sets the device placeholder; local-ios applies your personal UDID and app path; staging sets the backend URL your test code will use. They each own different keys, so they layer without conflict — exactly the disjoint-dimension design from earlier.

Each profile is independent and optional — you activate one only for a dimension you care about that run. If a run doesn't touch a backend, drop staging and run the device dimensions on their own:

mvn clean test -Dspring.profiles.active=ios,iphone17,local-ios

The only effect is that no environment overlay loads, so env.api-base-url stays unset — which is fine here, since nothing reads it yet. You'd add an environment profile precisely when a test starts needing that URL.

⚠️ When your staging and prod builds are different .apk/.ipa files. It's tempting to set driver.app-path in the staging document so the right build loads automatically. It won't work — your local file sets driver.app-path and is auto-discovered, so it always outranks anything set in an imported file like environments.yml. The environment value is silently ignored. To point at a different build, update your local file directly, or pass -Ddriver.app-path=... on the command line, which beats every file.

Step 2: Add Test Profiles — and Know Where the Line Is

A test profile controls how tests behave — retries, screenshot policy, timeouts. But "which tests run" is a separate question owned by a different mechanism entirely, and being precise about the split saves you a confusing afternoon.

Each phase document sets different run behavior — a smoke run fails fast with no retries, while a regression run tolerates one retry for flaky tests. Create config/phases.yml with one document per phase:

# Test phase profiles: how the run behaves
spring:
  config:
    activate:
      on-profile: smoke
test:
  retry-count: 0
  screenshot-on-failure: true

---
spring:
  config:
    activate:
      on-profile: regression
test:
  retry-count: 1
  screenshot-on-failure: true

Bind it the same way — core/src/main/java/com/mobileframework/config/TestRunConfig.java:

package com.mobileframework.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "test")
@Getter
@Setter
public class TestRunConfig {

    private int retryCount = 0;
    private boolean screenshotOnFailure = true;

}
⚠️ TestRunConfig binds today, but nothing reads retryCount or screenshotOnFailure yet. The retry mechanism is built in the parallel execution article and the screenshot-on-failure hook in the visual testing article. We define the configuration here so the shape is in place; the behavior plugs in later. That's deliberate — config-first, then the feature that consumes it.

Which scenarios run is a different mechanism entirely. Cucumber selects scenarios by tag, and that selection is owned by the Cucumber JUnit Platform engine — not by Spring. It reads cucumber.filter.tags from junit-platform.properties, so a Spring profile can't set it.

Tag your scenarios in the feature files:

@smoke
Scenario: User logs in with valid credentials
  ...

Then control selection through Cucumber's own configuration — either set a default in junit-platform.properties:

cucumber.filter.tags=@smoke

…or override it at launch with the matching system property:

mvn clean test -Dcucumber.filter.tags="@smoke"
💡 Two separate switches, usually set together. spring.profiles.active=...,smoke tells Spring how the run should behave — zero retries, screenshots on failure. cucumber.filter.tags="@smoke" tells Cucumber which scenarios to run. A CI smoke job always sets both: one without the other either runs the wrong scenarios or runs them with the wrong behavior.

Step 3: Activate Profiles from the CLI and CI — the Payoff

Locally, pass the profile combination as a system property:

mvn clean test -Dspring.profiles.active=staging,ios,iphone17,local-ios,smoke -Dcucumber.filter.tags="@smoke"

In CI, prefer the SPRING_PROFILES_ACTIVE environment variable over a -D flag. The reason: Maven forks a separate JVM to run tests, and -D system properties aren't always forwarded to that fork reliably. Environment variables always are. Spring Boot recognises SPRING_PROFILES_ACTIVE automatically — no extra configuration needed:

# Example CI step (provider-agnostic)
env:
  SPRING_PROFILES_ACTIVE: "prod,ios,iphone17,regression"
steps:
  - run: mvn clean test -Dcucumber.filter.tags="@regression"

That's the promise from Part 1 delivered. The pipeline picks the environment, the device, and the test phase by name. Nothing in src/ changes between an Android smoke run on staging and an iOS regression run on prod — only the profile list differs.

Step 4: Keep Secrets Out of the Repo

Cloud device farms and most real backends need credentials — an access key, an API token. Those must never be committed.

Secrets resolve from environment variables at runtime. Create config/application-cloud.properties — it follows the same Spring naming convention as the local files, so it's auto-discovered when the cloud profile is active. Reference secrets as ${...} placeholders; Spring substitutes the real value from the environment when the property is read:

# config/application-cloud.properties
cloud.username=${CLOUD_USERNAME}
cloud.access-key=${CLOUD_ACCESS_KEY}

The file is safe to commit — it contains references, never the secrets themselves. CI injects the real values as environment variables, and locally you export them in your shell. We'll use this exact pattern when wiring up device farms.

🚨 Never commit a real access key, token, or password to any properties file — not even briefly. Git history is forever; a key pushed once and deleted in the next commit is still a key you must now rotate. Always use a ${ENV_VAR} placeholder.

Configuration Map: Which Dimension Owns What

With both parts in place, here's the complete picture — every dimension, the file that holds it, the keys it owns, and how it's switched on:

Dimension Example profile file Owns these keys Activated by
Base (always loaded) application.properties driver.appium-url, spring.config.import, default spring.profiles.active n/a — always applied
Platform config/platforms.yml (android / ios docs) driver.platform, driver.app-package, driver.app-activity spring.profiles.active
Device config/devices.yml (pixel8, iphone17, … docs) driver.udid (placeholder only) spring.profiles.active
Local (personal, git-ignored) config/application-local-android.properties driver.udid, driver.app-path spring.profiles.active
Environment config/environments.yml (staging / prod docs) env.api-base-url spring.profiles.active
Test (behavior) config/phases.yml (smoke / regression docs) test.retry-count, test.screenshot-on-failure spring.profiles.active
Test (selection) junit-platform.properties cucumber.filter.tags cucumber.filter.tags system property
Secrets config/application-cloud.properties ${ENV_VAR} placeholders only spring.profiles.active + OS environment variables
💡 Reminder on precedence (covered in Part 1): a local .properties file is auto-discovered and outranks any imported .yml document; an environment variable beats every file; a -D system property beats everything. If an imported value "won't take," something higher in that order is overriding it — usually the same key set in two dimensions.

What's Next?

The next article — Parallel Test Execution for Mobile: How to Scale Without Breaking Everything — takes the configuration system you just built and runs many of these combinations at once. Thread-local drivers, test isolation, and the test.retry-count value you defined here but haven't wired up — that's where it comes alive. Parallel execution is where a single shared DriverManager quietly becomes a bug, and the profile-driven config you built today is exactly what lets each parallel thread know which device it owns.

Each article picks up exactly where the last one left off. If mvn clean test -Dspring.profiles.active=android,pixel8,local-android still passes on your emulator, the configuration foundation is solid — your CI pipeline will never need a file edit between runs, and neither will your routine local runs.

Get the Code

The complete, runnable workspace is available as a free download — everything built across Articles 2 through 6, with the full configuration system (both parts) wired together and ready to open in your IDE.

What's inside:

  • The full Maven multi-module project — root POM, core module, team-tests module
  • DriverManager from Article 3, refactored to inject the new typed DriverConfig
  • HomeScreen and LoginScreen with dual @AndroidFindBy / @iOSXCUITFindBy annotations from Article 5
  • Cucumber wiring from Article 4 — RunCucumberTest, CucumberSpringConfiguration, login.feature, junit-platform.properties
  • The new config package — DriverConfig, EnvironmentConfig, TestRunConfig
  • A base application.properties that imports the four dimension files — platforms.yml, devices.yml, environments.yml, phases.yml — each ready to edit
  • A .gitignore set up for application-local*.properties
💡 Before running, create src/test/resources/config/application-local-android.properties and/or config/application-local-ios.properties (both git-ignored). Set driver.udid to your emulator or simulator UDID and driver.app-path to your local app location. The committed platform profiles contain no personal values — everything machine-specific lives in your local file. Run mvn clean test from the project root — spring.profiles.active=android,pixel8,local-android is the default, so an Android emulator run works out of the box.

✅ Subscribe below to download the workspace .zip — free, just your email, and you'll get every new Framework Series article as it's published.

This post is for subscribers only

Become a member now and have access to all posts, enjoy exclusive content, and stay updated with constant updates.

Become a member

Already have an account? Sign in

  • Share on X
  • Share on Facebook
  • Share on LinkedIn
  • Share on Pinterest
  • Email

Written by

Mayvin Ramasawmy

Software engineer with 18 years of experience in development and test automation. From startups to enterprise — I build automation frameworks and share practical mobile testing knowledge at mobile-automation.io.

Montreal, Canada

Related Posts

Configuration Management for Mobile Tests, Part 1: Switch Platform and Device by Name
Featured Post

Configuration Management for Mobile Tests, Part 1: Switch Platform and Device by Name

Framework June 15, 2026
One Test, Two Platforms: Cross-Platform Mobile Testing with Page Object Model
For Members

One Test, Two Platforms: Cross-Platform Mobile Testing with Page Object Model

Framework June 8, 2026
BDD and Cucumber with Appium: Plain-English Tests for Your Framework
Featured Post For Members

BDD and Cucumber with Appium: Plain-English Tests for Your Framework

Framework June 1, 2026

Join the Mobile Automation Community

Receive new Appium and Maestro tutorials, framework design tips, and mobile testing best practices.

Please check your inbox and click the confirmation link.
Subscribe 1 Subscribe 2 Subscribe 3 Subscribe 1 Clone Subscribe 2 Clone Subscribe 3 Clone Subscribe 1 Clone
Subscribe 4 Subscribe 5 Subscribe 6 Subscribe 4 Clone Subscribe 5 Clone Subscribe 6 Clone Subscribe 4 Clone

Tags

Appium Best Practices Java Framework
© 2026 Mobile Automation
Mobile Automation
  • Home
  • Start Here
  • About
  • Blog
  • Tags
  • Contact
  • X
Sign in