Por Que POO Importa para la Automatizacion

La Programacion Orientada a Objetos no es solo un concepto academico — es la base de la automatizacion mantenible. Sin POO, los suites de tests crecen como scripts desestructurados imposibles de mantener a escala.

Entender POO te ayuda a escribir codigo de test organizado, reutilizable y facil de modificar cuando la aplicacion cambia.

Los Cuatro Principios de POO

1. Encapsulamiento

Encapsulamiento significa agrupar datos y metodos en una clase, ocultando detalles internos y exponiendo solo lo necesario.

Sin encapsulamiento:

// Selectores dispersos en tests — pesadilla de mantenimiento
test('usuario puede hacer login', async ({ page }) => {
  await page.fill('#email-input-v2', 'admin@test.com');
  await page.fill('input[name="pwd"]', 'secret');
  await page.click('.btn-submit-login');
});

test('usuario ve dashboard despues del login', async ({ page }) => {
  await page.fill('#email-input-v2', 'admin@test.com'); // duplicado
  await page.fill('input[name="pwd"]', 'secret');        // duplicado
  await page.click('.btn-submit-login');                  // duplicado
});

Con encapsulamiento (Page Object):

class LoginPage {
  #emailInput = '#email-input-v2';
  #passwordInput = 'input[name="pwd"]';
  #submitButton = '.btn-submit-login';

  constructor(page) {
    this.page = page;
  }

  async login(email, password) {
    await this.page.fill(this.#emailInput, email);
    await this.page.fill(this.#passwordInput, password);
    await this.page.click(this.#submitButton);
  }
}

// Tests limpios y mantenibles
test('usuario puede hacer login', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.login('admin@test.com', 'secret');
});

Cuando los selectores cambian, actualizas una clase en vez de docenas de tests.

2. Herencia

La herencia permite que una clase herede propiedades y metodos de otra. En testing, se usa para clases base y jerarquias de page objects.

// Pagina base con funcionalidad comun
class BasePage {
  constructor(page) {
    this.page = page;
  }

  async navigate(path) {
    await this.page.goto(`https://app.example.com${path}`);
  }

  async getTitle() {
    return await this.page.title();
  }

  async waitForPageLoad() {
    await this.page.waitForLoadState('networkidle');
  }
}

// LoginPage hereda de BasePage
class LoginPage extends BasePage {
  async login(email, password) {
    await this.navigate('/login');
    await this.page.fill('#email', email);
    await this.page.fill('#password', password);
    await this.page.click('#submit');
  }
}

// DashboardPage tambien hereda de BasePage
class DashboardPage extends BasePage {
  async getWelcomeMessage() {
    return await this.page.textContent('.welcome');
  }

  async navigateToSettings() {
    await this.page.click('#settings-link');
  }
}

Ambas paginas obtienen navigate(), getTitle() y waitForPageLoad() gratis a traves de herencia.

3. Polimorfismo

Polimorfismo significa que diferentes clases pueden usarse a traves de la misma interfaz. En testing, esto habilita helpers flexibles.

// Verificador base de notificaciones
class NotificationChecker {
  async verify(page, message) {
    throw new Error('La subclase debe implementar verify()');
  }
}

// Notificacion toast
class ToastChecker extends NotificationChecker {
  async verify(page, message) {
    await expect(page.locator('.toast')).toHaveText(message);
  }
}

// Notificacion banner
class BannerChecker extends NotificationChecker {
  async verify(page, message) {
    await expect(page.locator('.banner-alert')).toHaveText(message);
  }
}

// El codigo del test funciona con cualquier tipo de notificacion
async function verifyNotification(checker, page, message) {
  await checker.verify(page, message);
}

4. Abstraccion

Abstraccion significa ocultar la complejidad detras de una interfaz simple. Los tests deben leerse como una descripcion del comportamiento del usuario.

// Alta abstraccion — se lee como una historia de usuario
test('cliente puede realizar un pedido', async ({ page }) => {
  const shop = new ShopWorkflow(page);
  await shop.loginAsCustomer();
  await shop.addProductToCart('Mouse Inalambrico');
  await shop.proceedToCheckout();
  await shop.enterShippingAddress(testAddress);
  await shop.payWithCard(testCard);
  await shop.verifyOrderConfirmation();
});

La clase ShopWorkflow oculta toda la complejidad de selectores, waits y navegacion.

Jerarquia de Clases para Automatizacion

Un proyecto tipico tiene esta jerarquia:

BaseTest
├── WebTest (setup/teardown del browser)
│   ├── LoginTest
│   ├── DashboardTest
│   └── CheckoutTest
└── ApiTest (setup del cliente HTTP)
    ├── UserApiTest
    └── OrderApiTest

BasePage
├── LoginPage
├── DashboardPage
├── ProductPage
└── CheckoutPage
    ├── ShippingPage
    └── PaymentPage

Patron de Clase Base de Test

class BaseTest {
  constructor() {
    this.testData = {};
  }

  async setup() {
    // Sobreescribir en subclases
  }

  async teardown() {
    for (const [type, ids] of Object.entries(this.testData)) {
      for (const id of ids) {
        await this.cleanup(type, id);
      }
    }
  }

  trackCreated(type, id) {
    if (!this.testData[type]) this.testData[type] = [];
    this.testData[type].push(id);
  }
}

class WebTest extends BaseTest {
  async setup() {
    await super.setup();
    this.browser = await chromium.launch();
    this.page = await this.browser.newPage();
  }

  async teardown() {
    await this.browser.close();
    await super.teardown();
  }
}

Composicion vs Herencia

La herencia es poderosa pero puede crear jerarquias rigidas. La composicion ofrece mas flexibilidad.

Cuando Usar Herencia

Usa herencia para relaciones claras “es-un”:

  • LoginPage es una BasePage
  • WebTest es un BaseTest
  • AdminDashboard es un DashboardPage

Cuando Usar Composicion

Usa composicion cuando necesitas combinar capacidades no relacionadas:

// Composicion — mezclando capacidades
class TestHelper {
  constructor(page) {
    this.api = new ApiHelper();
    this.db = new DatabaseHelper();
    this.ui = new UIHelper(page);
    this.email = new EmailHelper();
  }

  async createUserAndLogin(userData) {
    const user = await this.api.createUser(userData);
    await this.ui.login(user.email, user.password);
    return user;
  }

  async verifyEmailReceived(to, subject) {
    return await this.email.waitForEmail(to, subject);
  }
}

La Regla General

  • La profundidad de herencia no debe exceder 3 niveles. Jerarquias profundas son dificiles de entender y modificar.
  • Favorece composicion al combinar diferentes dominios (API + UI + DB).
  • Usa herencia dentro de un solo dominio (jerarquia de page objects).

Interfaces y Contratos

Incluso en JavaScript (que carece de interfaces formales), puedes definir contratos:

class PageObject {
  async goto() { throw new Error('No implementado'); }
  async verifyLoaded() { throw new Error('No implementado'); }
  get urlPattern() { throw new Error('No implementado'); }
}

class LoginPage extends PageObject {
  async goto() {
    await this.page.goto('/login');
  }

  async verifyLoaded() {
    await expect(this.page.locator('#login-form')).toBeVisible();
  }

  get urlPattern() {
    return /\/login$/;
  }
}

Vista Previa de Patrones de Diseno

POO habilita patrones poderosos en automatizacion:

PatronPropositoLeccion
Page Object ModelEncapsular interacciones de pagina8.8
Screenplay PatternOrganizacion basada en actores8.9
Factory PatternCrear objetos de datos de prueba8.23
Builder PatternConstruir datos complejos8.23
Strategy PatternIntercambiar algoritmos en runtime8.28

Ejercicio: Construye una Jerarquia de Page Objects

Crea una jerarquia de clases de tres niveles para una aplicacion e-commerce:

  1. BasePage — metodos comunes: navigate, getTitle, waitForLoad
  2. ProductListPage extends BasePage — metodos: searchProduct, filterByCategory, sortBy
  3. ProductDetailPage extends BasePage — metodos: addToCart, selectSize, getPrice
  4. CartPage extends BasePage — metodos: updateQuantity, removeItem, proceedToCheckout

Para cada clase, identifica que metodos y propiedades deben ser publicos vs privados.

Puntos Clave

  • El encapsulamiento oculta selectores y detalles dentro de page objects
  • La herencia reduce duplicacion a traves de clases base y jerarquias de paginas
  • El polimorfismo habilita componentes de test flexibles e intercambiables
  • La abstraccion hace que los tests se lean como historias de usuario
  • Prefiere composicion sobre jerarquias de herencia profundas
  • Mantiene la profundidad de herencia en maximo 3 niveles