Ao longo de quase uma década e meia construindo e reconstruindo suites de automação, testemunhei um padrão (sem trocadilho) repetido à exaustão: projetos começam lindos, organizados, promissores. Seis meses depois, só um Frankenstein de código duplicado, lógica de negócio vazando para os testes e manutenção que consome 70% do tempo do time.
O problema raramente é a ferramenta (Cypress, Playwright, Selenium, o que vier amanhã). O problema é arquitetural. E a boa notícia é que os Design Patterns de teste bem aplicados são imortais. Eles transcendem linguagens e frameworks porque resolvem problemas humanos de organização, não técnicos de sintaxe.
Vamos ao que interessa: os padrões que realmente entregam valor em 2026, com exemplos práticos e armadilhas fatais.
- Page Object Model (POM) Clássico, Mas Nem Todo Mundo Sabe Usar
O POM é o avô dos patterns de automação. Todo mundo conhece, mas poucos aplicam corretamente. O princípio é simples: cada página ou componente da UI é representado por uma classe que expõe apenas ações de alto nível.
Exemplo do que NÃO fazer (anti-pattern visto semanalmente):
class LoginPage: username_input = "#username" password_input = "#password" login_button = "#login"
def type_username(self, text): driver.find_element(self.username_input).send_keys(text)
Isso não é um Page Object, é um repositório de seletores com métodos anêmicos. O verdadeiro POM encapsula comportamento, não elementos.
Exemplo correto:
class LoginPage: def __init__(self, page): self.page = page
def login_as(self, user_type): """Ação de alto nível que encapsula toda a sequência""" if user_type == "standard": self._fill_credentials("standard_user", "secret_sauce") elif user_type == "locked": self._fill_credentials("locked_user", "secret_sauce") self.click_login() return InventoryPage(self.page) def get_error_message(self): return self.page.locator("[data-test='error']").text_content() def _fill_credentials(self, username, password): self.page.fill("#user-name", username) self.page.fill("#password", password) def click_login(self): self.page.click("#login-button")
A regra de ouro: Um método de Page Object deve retornar outro Page Object (fluxo feliz) ou dados (extração de informação). Nunca expor elementos ou ações cruas.
- Test Data Factory: O Pattern Mais Subestimado
O assassino silencioso da produtividade: testes que dependem de dados hardcoded.
def test_login(): user = "john.doe@example.com" password = "Test@123"
class UserFactory: @staticmethod def standard_user(): return User( email = f"test+{uuid4()}@example.com", password = "P@ssw0rd!", role = "customer" )
@staticmethod def admin_user(): return User( email = f"admin+{uuid4()}@example.com", password = "Admin@456", role = "admin", permissions = ["read", "write", "delete"] )
- Builder Pattern Para Cenários de Dados Complexos
Quando sua entidade tem 15+ campos opcionais, o Builder impede a explosão de construtores.
class OrderBuilder: def __init__(self): self.order = {"customer_id": 1, "items": []}
def with_items(self, items): self.order["items"] = items return self def with_coupon(self, code): self.order["coupon_code"] = code return self def build(self): return self.order order = (OrderBuilder() .with_items([product1, product2]) .with_coupon("BLACKFRIDAY") .build())
- Facade Pattern Orquestrando Múltiplos Page Objects
class CheckoutFacade: def __init__(self, page): self.cart = CartPage(page) self.checkout = CheckoutPage(page)
def complete_purchase(self, user, cart_items): self.cart.add_items(cart_items) self.checkout.fill_shipping(user.address) return self.confirmation.get_order_number() def test_guest_checkout(): facade = CheckoutFacade(page) order_id = facade.complete_purchase(user, products) assert order_id is not None
- Strategy Pattern Lidando com Múltiplos Contextos
class AuthenticationStrategy(ABC): @abstractmethod def login(self, credentials): pass
class WebAuthStrategy(AuthenticationStrategy): def login(self, credentials): self.page.goto("/login") self.page.fill("#email", credentials.email)
class APIAuthStrategy(AuthenticationStrategy): def login(self, credentials): response = self.api_client.post("/auth/login", json=credentials) self.token = response.json()["token"]
def test_user_profile(auth_strategy): auth_strategy.login(test_user)
- Singleton Pattern (Com Moderação) Para Recursos Caros
class DriverManager: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance._initialize() return cls._instance
Cuidado: Singleton pode esconder problemas de paralelização. Prefira injeção de dependência quando possível.
- Repository Pattern Centralizando Seletores
class LoginRepository: USERNAME_INPUT = "input[data-test='username']" PASSWORD_INPUT = "input[data-test='password']" LOGIN_BUTTON = "input[data-test='login-button']"
@staticmethod def get_user_menu_item(user_name): return f"div.user-menu:has-text('{user_name}')"
Vantagem: Quando um dev renomeia data-test-id, você altera em UM lugar.
Conclusão: Padrões Não São Receita de Bolo
Aprendi da pior maneira: aplicar todos os padrões de uma vez é tão ruim quanto aplicar nenhum. O segredo é evolução incremental.
Comece com POM. Quando sentir dor de dados repetidos, adicione Factory. Quando os testes ficarem muito longos, introduza Facade.
A boa arquitetura não é imposta desde o dia 1: ela emerge para resolver dores reais do time. E a melhor métrica de sucesso é simples: quanto tempo leva para você adicionar um teste para uma nova funcionalidade? Se a resposta for "minutos", você acertou.
Agora vá e organize esse legado. Ou comece certo do zero. Seu eu do futuro (e o onboarding do próximo QA) vão te agradecer.
QA Sênior que já refatorou mais código de teste do que gostaria de admitir