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”:
LoginPagees unaBasePageWebTestes unBaseTestAdminDashboardes unDashboardPage
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:
| Patron | Proposito | Leccion |
|---|---|---|
| Page Object Model | Encapsular interacciones de pagina | 8.8 |
| Screenplay Pattern | Organizacion basada en actores | 8.9 |
| Factory Pattern | Crear objetos de datos de prueba | 8.23 |
| Builder Pattern | Construir datos complejos | 8.23 |
| Strategy Pattern | Intercambiar algoritmos en runtime | 8.28 |
Ejercicio: Construye una Jerarquia de Page Objects
Crea una jerarquia de clases de tres niveles para una aplicacion e-commerce:
BasePage— metodos comunes: navigate, getTitle, waitForLoadProductListPage extends BasePage— metodos: searchProduct, filterByCategory, sortByProductDetailPage extends BasePage— metodos: addToCart, selectSize, getPriceCartPage 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