Automatización de pruebas móviles con Appium
Configura y ejecuta pruebas automatizadas de apps móviles en iOS y Android usando Appium con WebDriver, gestionando capabilities, gestos y flujos de CI.
Descarga abierta · sin registro · para iOS, Android, Node.js
// ============================================================================= // FitTrack Mobile — Suite de Pruebas Appium 2.x + WebDriverIO // Cliente: FitTrack | Versión app: 2.4.1-staging // Plataformas: Android (UiAutomator2) + iOS (XCUITest) // Autor: CULTIVA IA — Automatización Móvil // =============================================================================
// ----------------------------------------------------------------------------- // capabilities.js — Desired capabilities para Android e iOS // -----------------------------------------------------------------------------
const androidCaps = { platformName: 'Android', 'appium:automationName': 'UiAutomator2', 'appium:deviceName': 'Pixel_7_API_34', 'appium:platformVersion': '14', 'appium:app': './builds/fittrack-staging-2.4.1.apk', 'appium:autoGrantPermissions': true, 'appium:noReset': false, 'appium:newCommandTimeout': 90, 'appium:appPackage': 'com.fittrack.app', 'appium:appActivity': 'com.fittrack.app.MainActivity', };
const iosCaps = { platformName: 'iOS', 'appium:automationName': 'XCUITest', 'appium:deviceName': 'iPhone 15', 'appium:platformVersion': '17.4', 'appium:app': './builds/fittrack-staging-2.4.1.ipa', 'appium:autoAcceptAlerts': true, 'appium:newCommandTimeout': 90, 'appium:bundleId': 'com.fittrack.app', 'appium:wdaLaunchDelay': 2000, };
// ----------------------------------------------------------------------------- // wdio.conf.js — Configuración WebDriverIO multi-plataforma // -----------------------------------------------------------------------------
exports.config = { runner: 'local', hostname: 'localhost', port: 4723, path: '/',
specs: ['./tests/**/*.spec.js'], exclude: [],
maxInstances: 1,
capabilities: [ { ...androidCaps, // Suite específica Android 'wdio:maxInstances': 1, }, ],
logLevel: 'info', bail: 0, waitforTimeout: 15000, connectionRetryTimeout: 120000, connectionRetryCount: 3,
services: [ ['appium', { command: 'appium', args: { relaxedSecurity: false, log: './logs/appium.log', }, }], ],
framework: 'mocha',
reporters: [
'spec',
['allure', {
outputDir: 'allure-results',
disableWebdriverStepsReporting: false,
disableWebdriverScreenshotsReporting: false,
}],
['junit', {
outputDir: './reports/junit',
outputFileFormat: (opts) => results-${opts.cid}.${opts.capabilities.platformName}.xml,
}],
],
mochaOpts: { ui: 'bdd', timeout: 60000, },
// Hook: captura screenshot en caso de fallo
afterTest: async function (test, _context, { passed }) {
if (!passed) {
await browser.saveScreenshot(./reports/screenshots/${test.title.replace(/\s/g, '_')}_FAIL.png);
}
},
};
// ----------------------------------------------------------------------------- // tests/helpers/gestures.js — Gestos móviles reutilizables // -----------------------------------------------------------------------------
const Gestures = { /**
- Scroll vertical en un contenedor scrollable
- @param {string} direction - 'up' | 'down'
- @param {number} percent - porcentaje del scroll (0.0–1.0) */ async scrollVertical(direction = 'down', percent = 0.8) { const { width, height } = await driver.getWindowSize(); await driver.execute('mobile: scrollGesture', { left: Math.round(width * 0.1), top: Math.round(height * 0.2), width: Math.round(width * 0.8), height: Math.round(height * 0.6), direction, percent, }); },
/**
- Swipe horizontal sobre un elemento (p.ej. descartar card)
- @param {WebdriverIO.Element} element
- @param {string} direction - 'left' | 'right' */ async swipeElement(element, direction = 'left') { await driver.execute('mobile: swipeGesture', { elementId: element.elementId, direction, percent: 0.75, }); },
/**
- Long press sobre un elemento
- @param {WebdriverIO.Element} element
- @param {number} durationMs */ async longPress(element, durationMs = 1500) { await driver.execute('mobile: longClickGesture', { elementId: element.elementId, duration: durationMs, }); },
/**
- Pinch to zoom (ampliar) sobre la pantalla */ async pinchZoomIn() { const { width, height } = await driver.getWindowSize(); await driver.execute('mobile: pinchOpenGesture', { left: Math.round(width * 0.25), top: Math.round(height * 0.25), width: Math.round(width * 0.5), height: Math.round(height * 0.5), percent: 0.75, }); }, };
module.exports = Gestures;
// ----------------------------------------------------------------------------- // tests/auth/login.spec.js — Pruebas de autenticación // -----------------------------------------------------------------------------
describe('[Auth] Pantalla de Login — FitTrack', () => {
beforeEach(async () => { // Reinicia la app al estado inicial antes de cada test await driver.reset(); // Esperar splash + carga inicial await $('~splash-screen').waitForExist({ timeout: 8000, reverse: true }); });
it('✓ Login exitoso con credenciales válidas', async () => { const emailField = await $('~login-email-input'); await emailField.waitForDisplayed({ timeout: 10000 }); await emailField.setValue('carlos.ruiz@fittrack.com');
const passwordField = await $('~login-password-input');
await passwordField.setValue('Fittr@ck2024!');
await $('~login-submit-button').click();
// Verificar que llegamos al Dashboard
const dashboardTitle = await $('~dashboard-header-title');
await dashboardTitle.waitForDisplayed({ timeout: 12000 });
await expect(dashboardTitle).toHaveText('Hola, Carlos');
});
it('✓ Muestra error con contraseña incorrecta', async () => { await $('~login-email-input').setValue('carlos.ruiz@fittrack.com'); await $('~login-password-input').setValue('contraseña_incorrecta'); await $('~login-submit-button').click();
const errorBanner = await $('~login-error-banner');
await errorBanner.waitForDisplayed({ timeout: 6000 });
await expect(errorBanner).toHaveText('Credenciales incorrectas. Inténtalo de nuevo.');
});
it('✓ Validación inline — campo email vacío', async () => { // Pulsa submit sin rellenar nada await $('~login-submit-button').click();
const emailError = await $('~login-email-error');
await emailError.waitForDisplayed({ timeout: 4000 });
await expect(emailError).toHaveText('Introduce tu email');
});
it('✓ Navega a pantalla de registro', async () => { await $('~login-signup-link').click(); const registerTitle = await $('~register-screen-title'); await registerTitle.waitForDisplayed({ timeout: 6000 }); await expect(registerTitle).toHaveText('Crea tu cuenta'); });
it('✓ Flujo "Olvidé mi contraseña"', async () => { await $('~login-forgot-password-link').click(); const resetTitle = await $('~reset-password-title'); await resetTitle.waitForDisplayed({ timeout: 6000 }); await $('~reset-email-input').setValue('carlos.ruiz@fittrack.com'); await $('~reset-submit-button').click(); const confirmation = await $('~reset-confirmation-message'); await confirmation.waitForDisplayed({ timeout: 8000 }); await expect(confirmation).toHaveText('Te hemos enviado un correo de recuperación'); }); });
// ----------------------------------------------------------------------------- // tests/workout/register-workout.spec.js — Registro de entrenamiento // -----------------------------------------------------------------------------
describe('[Workout] Registrar entrenamiento — FitTrack', () => {
before(async () => { // Login previo para llegar al estado autenticado await driver.reset(); await $('~splash-screen').waitForExist({ timeout: 8000, reverse: true }); await $('~login-email-input').setValue('carlos.ruiz@fittrack.com'); await $('~login-password-input').setValue('Fittr@ck2024!'); await $('~login-submit-button').click(); await $('~dashboard-header-title').waitForDisplayed({ timeout: 12000 }); });
it('✓ Añadir nuevo ejercicio al entrenamiento', async () => { // Navegar a sección Entrenamientos await $('~tab-workouts').click(); await $('~workouts-start-new-button').click();
// Seleccionar tipo de entrenamiento
const workoutTypeModal = await $('~workout-type-modal');
await workoutTypeModal.waitForDisplayed({ timeout: 6000 });
await $('~workout-type-strength').click();
// Añadir ejercicio "Press de banca"
await $('~add-exercise-button').click();
const searchInput = await $('~exercise-search-input');
await searchInput.setValue('Press de banca');
await $('~exercise-result-press-banca').click();
// Configurar serie: 4 sets x 10 reps x 80 kg
await $('~exercise-sets-input').setValue('4');
await $('~exercise-reps-input').setValue('10');
await $('~exercise-weight-input').setValue('80');
await $('~exercise-save-button').click();
// Verificar que aparece en la lista
const addedExercise = await $('~exercise-list-item-press-banca');
await addedExercise.waitForDisplayed({ timeout: 6000 });
await expect(addedExercise).toHaveText('Press de banca — 4×10 @ 80 kg');
});
it('✓ Registrar set completado con tiempo de descanso', async () => { // Asumimos que ya hay un ejercicio en la sesión activa const firstSet = await $('~set-row-1'); await firstSet.waitForDisplayed({ timeout: 6000 });
// Marcar como completado
await $('~set-row-1-complete-button').click();
// Verificar que inicia el temporizador de descanso
const restTimer = await $('~rest-timer-display');
await restTimer.waitForDisplayed({ timeout: 4000 });
await expect(restTimer).toBeDisplayed();
});
it('✓ Finalizar y guardar entrenamiento', async () => { await $('~finish-workout-button').click();
// Confirmar en modal
const confirmModal = await $('~finish-workout-confirm-modal');
await confirmModal.waitForDisplayed({ timeout: 5000 });
await $('~finish-workout-confirm-button').click();
// Debe navegar al resumen del entrenamiento
const summaryTitle = await $('~workout-summary-title');
await summaryTitle.waitForDisplayed({ timeout: 8000 });
await expect(summaryTitle).toHaveText('Entrenamiento completado');
// Verificar volumen total mostrado
const totalVolume = await $('~workout-summary-volume');
await expect(totalVolume).toBeDisplayed();
}); });
// ----------------------------------------------------------------------------- // tests/dashboard/progress.spec.js — Dashboard y progreso // -----------------------------------------------------------------------------
describe('[Dashboard] Progreso semanal — FitTrack', () => {
before(async () => { await driver.reset(); await $('~splash-screen').waitForExist({ timeout: 8000, reverse: true }); await $('~login-email-input').setValue('carlos.ruiz@fittrack.com'); await $('~login-password-input').setValue('Fittr@ck2024!'); await $('~login-submit-button').click(); await $('~dashboard-header-title').waitForDisplayed({ timeout: 12000 }); });
it('✓ Dashboard carga gráfica de progreso semanal', async () => { const progressChart = await $('~weekly-progress-chart'); await progressChart.waitForDisplayed({ timeout: 10000 }); await expect(progressChart).toBeDisplayed(); });
it('✓ Scroll hasta tarjetas de estadísticas', async () => { const Gestures = require('../helpers/gestures'); await Gestures.scrollVertical('down', 0.6);
const statsCard = await $('~stats-card-total-workouts');
await statsCard.waitForDisplayed({ timeout: 6000 });
await expect(statsCard).toBeDisplayed();
});
it('✓ Navegar a semana anterior en el gráfico', async () => { // Swipe derecha sobre el gráfico para ver semana anterior const chart = await $('~weekly-progress-chart'); const Gestures = require('../helpers/gestures'); await Gestures.swipeElement(chart, 'right');
const weekLabel = await $('~chart-week-label');
await weekLabel.waitForDisplayed({ timeout: 4000 });
// La etiqueta debe cambiar a la semana anterior
await expect(weekLabel).not.toHaveText('Esta semana');
});
it('✓ Tarjeta de racha de días muestra valor correcto', async () => { const streakCard = await $('~streak-days-card'); await streakCard.waitForDisplayed({ timeout: 6000 }); const streakValue = await $('~streak-days-value'); // Debe mostrar un número >= 0 const text = await streakValue.getText(); await expect(parseInt(text, 10)).toBeGreaterThanOrEqual(0); }); });
// ----------------------------------------------------------------------------- // tests/profile/edit-profile.spec.js — Edición de perfil de usuario // -----------------------------------------------------------------------------
describe('[Perfil] Edición de datos de usuario — FitTrack', () => {
before(async () => { await driver.reset(); await $('~splash-screen').waitForExist({ timeout: 8000, reverse: true }); await $('~login-email-input').setValue('carlos.ruiz@fittrack.com'); await $('~login-password-input').setValue('Fittr@ck2024!'); await $('~login-submit-button').click(); await $('~dashboard-header-title').waitForDisplayed({ timeout: 12000 }); // Navegar al perfil await $('~tab-profile').click(); await $('~profile-screen-title').waitForDisplayed({ timeout: 6000 }); });
it('✓ Editar nombre de usuario', async () => { await $('~profile-edit-button').click(); const nameField = await $('~profile-name-input'); await nameField.waitForDisplayed({ timeout: 5000 }); await nameField.clearValue(); await nameField.setValue('Carlos R. Gómez'); await $('~profile-save-button').click();
const displayedName = await $('~profile-display-name');
await displayedName.waitForDisplayed({ timeout: 6000 });
await expect(displayedName).toHaveText('Carlos R. Gómez');
});
it('✓ Actualizar peso corporal', async () => { await $('~profile-edit-button').click(); const weightField = await $('~profile-weight-input'); await weightField.waitForDisplayed({ timeout: 5000 }); await weightField.clearValue(); await weightField.setValue('78.5'); await $('~profile-save-button').click();
const weightDisplay = await $('~profile-weight-display');
await expect(weightDisplay).toHaveText('78.5 kg');
});
it('✓ Cambiar objetivo de entrenamiento', async () => { await $('~profile-edit-button').click(); await $('~profile-goal-selector').click(); const goalModal = await $('~goal-selector-modal'); await goalModal.waitForDisplayed({ timeout: 5000 }); await $('~goal-option-muscle-gain').click(); await $('~profile-save-button').click();
const goalDisplay = await $('~profile-goal-display');
await expect(goalDisplay).toHaveText('Ganancia muscular');
}); });
// ----------------------------------------------------------------------------- // package.json (dependencias del proyecto de pruebas) // ----------------------------------------------------------------------------- /* { "name": "fittrack-appium-tests", "version": "1.0.0", "description": "Suite de pruebas E2E móviles para FitTrack con Appium 2 + WebDriverIO", "scripts": { "test:android": "wdio run wdio.conf.android.js", "test:ios": "wdio run wdio.conf.ios.js", "test:all": "npm run test:android && npm run test:ios", "report": "allure generate allure-results --clean -o allure-report && allure open allure-report", "appium:start": "appium --relaxed-security --log ./logs/appium.log" }, "devDependencies": { "@wdio/allure-reporter": "^8.36.0", "@wdio/appium-service": "^8.36.0", "@wdio/cli": "^8.36.0", "@wdio/local-runner": "^8.36.0", "@wdio/mocha-framework": "^8.36.0", "@wdio/spec-reporter": "^8.36.0", "@wdio/junit-reporter": "^8.36.0", "appium": "^2.5.4", "appium-uiautomator2-driver": "^3.4.0", "appium-xcuitest-driver": "^7.8.0", "allure-commandline": "^2.27.0" } } */
// ----------------------------------------------------------------------------- // .github/workflows/mobile-tests.yml — CI con GitHub Actions // ----------------------------------------------------------------------------- /* name: FitTrack Mobile E2E Tests
on: push: branches: [main, staging] pull_request: branches: [main]
jobs: android-tests: runs-on: ubuntu-latest timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Set up Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Set up Java 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Set up Android SDK
uses: android-actions/setup-android@v3
- name: Create AVD (Pixel 7 API 34)
run: |
echo "y" | avdmanager create avd \
--name "Pixel_7_API_34" \
--package "system-images;android-34;google_apis;x86_64" \
--device "pixel_7"
- name: Start Android Emulator
run: |
$ANDROID_HOME/emulator/emulator -avd Pixel_7_API_34 \
-no-audio -no-window -gpu swiftshader_indirect &
adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 5; done'
- name: Install test dependencies
run: npm ci
- name: Download staging APK
run: |
curl -L "${{ secrets.STAGING_APK_URL }}" \
-o ./builds/fittrack-staging-2.4.1.apk
- name: Run Android tests
run: npm run test:android
- name: Upload test reports
uses: actions/upload-artifact@v4
if: always()
with:
name: android-test-reports
path: |
reports/
allure-results/
logs/
- name: Publish JUnit results
uses: dorny/test-reporter@v1
if: always()
with:
name: Android Test Results
path: reports/junit/*.xml
reporter: java-junit
*/
// qué_hace
Automatiza pruebas de apps móviles nativas, híbridas y web en iOS y Android usando el protocolo WebDriver de Appium.
// cómo_lo_hace
Guía la configuración de drivers XCUITest/UiAutomator2, capabilities, scripts de test en JS y Python, gestos táctiles y configuración de WebDriverIO para CI.
// ejemplo_de_uso
Úsala cuando tu equipo de QA necesita cubrir flujos de una app móvil en iOS y Android sin probar manualmente en cada dispositivo. Ej.: automatizas el flujo de registro y pago en la app de iOS con Appium y lo integras en la pipeline de CI.
// plataformas
// opiniones_de_la_comunidad
Opiniones
Cargando opiniones…
// pase_cultiva_ia
Llévate todo el arsenal con el Pase
Todas las skills, prompts y automatizaciones del catálogo en un único archivo, listas para usar: un pago, acceso de por vida y las novedades que añadamos. Sin suscripción.
Pago único · IVA incluido · pago seguro con Stripe.
Acceso inmediato · si no es lo que esperabas, te devolvemos los 10 €.