📆 ⏱️ 7 min de lectura

🧪 Aplicando ATDD

Cómo usé Acceptance Test-Driven Development para solucionar un problemilla en la pantalla de ranking en uno de mis pet projects

Introducción

Hace unos días decidí usar una pequeña app de trivial de preguntas de cine para iOS que desarrollé hace un tiempecito para practicar buenas prácticas de testing, especialmente ATDD (Acceptance Test-Driven Development), que estoy aprendiendo gracias al curso Testing: Introducción y buenas prácticas de CodelyTV.

En este post quiero compartir cómo detecté y solucioné un pequeño problema de experiencia de usuario en la pantalla de ranking de puntuaciones, aplicando ATDD.

El problema detectado

En la app, los usuarios pueden consultar un ranking con las mejores puntuaciones y, si lo desean, pueden limpiar el ranking pulsando un botón. El problema es que, tras limpiar el ranking, la pantalla no se actualiza automáticamente: hay que salir y volver a entrar para ver que realmente se han eliminado los registros.

El objetivo: que al pulsar el botón de limpiar, la lista de puntuaciones se vaciara al instante.

¿Por qué ATDD?

ATDD (Acceptance Test-Driven Development) es una metodología que pone el foco en definir primero el comportamiento esperado desde el punto de vista del usuario, antes de implementar la solución técnica. Me pareció la excusa perfecta para practicarlo en este caso, ya que el problema era muy concreto y afectaba directamente a la experiencia de usuario.

Escribiendo el test de aceptación

Siguiendo la filosofía ATDD, lo primero que hice fue escribir un test de aceptación que describe el comportamiento esperado desde el punto de vista del usuario. En este caso, quiero asegurarme de que, al pulsar el botón para limpiar el ranking, la lista de puntuaciones desaparece y se muestra la vista vacía, sin necesidad de recargar la pantalla manualmente.

  • Para poder probar bien esta funcionalidad, he añadido un mock del ranking con 3 resultados iniciales. Así puedo comprobar que la lista se muestra correctamente al inicio y que realmente se vacía tras pulsar el botón.

El test queda así:

func testLimpiarRankingDejaListaVacia() {
    let app = XCUIApplication()
    app.launchArguments.append("--mockRanking")
    app.launch()

    // Comprobar que al inicio hay tres elementos en el ranking
    let rankingList = app.collectionViews["ranking_list"]
    XCTAssertEqual(rankingList.cells.count, 3) // Esperamos 3 celdas

    // Pulsar el botón para limpiar el ranking
    let cleanButton = app.buttons["clean_ranking_button"]
    cleanButton.tap()

    // Confirmar el borrado en el alert
    let confirmButton = app.buttons["Delete"]
    confirmButton.tap()

    // Comprobar que la lista ya no existe (la UI debería actualizarse)
    XCTAssertFalse(rankingList.waitForExistence(timeout: 1))

    // Comprobar que se muestra la vista vacía
    let emptyViewHeadline = app.staticTexts["empty_ranking_headline"]
    XCTAssert(emptyViewHeadline.waitForExistence(timeout: 1))
}

Este test debe fallar la primera vez que lo ejecutamos, ya que todavía no hemos implementado la lógica. Este fallo nos asegura que el test realmente está comprobando el comportamiento que queremos conseguir.

Implementando la lógica: entrando en el ciclo TDD

Una vez tengo el test de aceptación fallando, es el momento de entrar en el ciclo TDD clásico para implementar la lógica necesaria que hará que el test pase. Aquí es donde se empieza a trabajar de dentro hacia fuera: primero se escriben tests unitarios sobre la lógica interna, se comprueba que todo funciona, y estos cambios deberían hacer pasar el test de aceptación.

Test unitario para la lógica interna

Este test unitario asegura de que la función que borra el ranking funciona correctamente en el ViewModel.

El test queda así:

func testEraseRanking() {
    // Given: un ranking con tres elementos
    viewModel.ranking = [
        Score(name: "PLAYER1", score: 123),
        Score(name: "PLAYER2", score: 45),
        Score(name: "PLAYER3", score: 34)
    ]

    // When: borro el ranking
    viewModel.eraseRanking()

    // Then: el ranking debe estar vacío
    XCTAssert(viewModel.ranking.isEmpty)
}

Lo mismo de antes: este test debe fallar la primera vez que lo ejecuto, ya que todavía no he implementado la lógica… Espera ¿Qué? ¿Por qué pasa el test?

Un caso real: cuando la lógica funciona pero la UI no reacciona

Aquí me encontré con una situación muy interesante: La función para borrar el ranking ya existía y el test unitario pasaba sin problemas, es decir, el array de puntuaciones se vaciaba correctamente. Sin embargo, el test de aceptación seguía fallando porque la interfaz no se actualizaba tras limpiar el ranking.

Este tipo de situaciones son las que realmente justifican el esfuerzo de escribir tests de aceptación: nos ayudan a garantizar que la app responde como espera el usuario, no solo que los métodos hacen lo que deben a nivel interno.

El problema técnico

Después de investigar un poco, descubrí el problema: estaba testando que se borraba el ranking de la propiedad var ranking: [Score] = [] del ViewModel, pero la vista de ranking tenía su propia lógica de estado que no estaba conectada correctamente con el ViewModel.

La solución fue añadir @Published al ranking del ViewModel para que los cambios se reflejen automáticamente en la UI. Antes no lo tenía porque no usaba esa variable para pintar los datos, sino una variable interna de la vista.

Imagina que tienes dos copias de la misma información:

  • Una en el ViewModel (que es la que yo estaba testando)
  • Otra en la vista (que es la que el usuario ve)

Mi test unitario verificaba que se borraba la copia del ViewModel, pero la vista seguía mostrando su copia local. Es como si tuvieras dos agendas: borras una entrada de tu agenda del trabajo, pero tu agenda personal sigue mostrando la cita porque no sabía que la habías borrado.

La solución: una sola fuente de verdad

La clave fue asegurarme de que la vista siempre use la información del ViewModel, no una copia local. Cambié la estructura para que:

  1. El ViewModel sea la única fuente de verdad del ranking
  2. Los cambios del ranking se reflejen el a UI añadiendo @Published a la propiedad ranking del ViewModel

Así, cuando se borra el ranking del viewModel, la vista se entera automáticamente y se repinta.

¿Por qué esto es importante?

Sin el test de aceptación, nunca se habría detectado que aunque la lógica funcionaba, el usuario no veía el cambio. Y sin el test unitario, no habría tenido la confianza de que el problema estaba en la conexión entre ViewModel y vista, no en la lógica de borrado.

El test de aceptación pasando

Una vez implementada la solución, el test de aceptación finalmente pasa:

Test de aceptación pasando: la UI reacciona correctamente al limpiar el ranking

¡Ahora sí, la UI reacciona correctamente y el test pasa!

Reflexión final

Practicar lo aprendido es la mejor forma de que los conceptos realmente se queden contigo. No es lo mismo leer sobre ATDD y TDD que enfrentarte a un problema real y ver cómo estas metodologías te ayudan a solucionarlo paso a paso. Este proyecto es antiguo y tiene mil cosas mejorables, hasta yo me he asustado viendo el código, no me escondo, pero lo importante era experimentar con ATDD para resolver un problema concreto, y eso se ha conseguido.

Key points:

  • ATDD te ayuda a pensar en el usuario: primero define el comportamiento esperado, luego implementa la solución.
  • Los tests de aceptación y los unitarios se complementan: uno valida la experiencia, el otro la lógica interna.
  • Una sola fuente de verdad en el estado evita líos y bugs difíciles de detectar.
  • Practicar con proyectos reales (aunque sean antiguos o “feos”) es la mejor forma de aprender.
  • No hace falta que el código sea perfecto para aplicar buenas prácticas y aprender algo nuevo.

¿Te ha resultado útil este artículo? ¿Tienes alguna experiencia similar con ATDD o TDD que quieras compartir? ¡Me encantaría leerte!

Nos leemos 🫶

Aplicando ATDD en una app de trivial de cine 🎥 ¿Te vienes? dianait.blog/blog/aplican...

[image or embed]

— Diana Hernández (@dianait.dev) 19 July 2025 at 10:45