Auditar: scanner, tests y proceso
Todo lo aprendido en el nivel se degrada en cuestión de semanas si nada lo sostiene. Esta lección construye el andamiaje de verificación en tres capas: el Accessibility Scanner y qué clase de defectos detecta y cuáles le resultan invisibles, la comprobación automatizada dentro de los tests de Compose mediante los asertos semánticos y el marco de validación de accesibilidad, y la integración de todo ello en el proceso de revisión y en la integración continua para que el fallo aparezca antes del `merge` y no en una reseña de una estrella en Google Play.
La accesibilidad tiene una propiedad económica desagradable: se construye con esfuerzo deliberado y se destruye por accidente. Basta con que alguien extraiga un componente a una función reutilizable y pierda por el camino el modificador de fusión, o con que una refactorización sustituya un Text por un Icon sin descripción, o con que un rediseño aclare un gris dos tonos. Ninguno de esos cambios rompe una compilación, ninguno falla un test funcional y ninguno se ve en una captura de pantalla. La consecuencia es predecible y universal: los equipos que hacen una campaña de accesibilidad y no montan verificación vuelven a su estado anterior en dos o tres trimestres, sin que nadie note en qué momento ocurrió. Auditar no es, por tanto, la última etapa del trabajo, sino la única parte que lo vuelve permanente. Y como todo lo permanente en ingeniería, funciona cuando es automático, barato y ruidoso en el momento exacto en que el defecto se introduce.
- Utilizar el
Accessibility Scannercon criterio, sabiendo qué familia de defectos detecta y cuál no puede detectar. - Escribir asertos semánticos en los tests de Compose que fijen el contrato de accesibilidad de una pantalla.
- Activar la validación automática de accesibilidad dentro de los tests instrumentados existentes.
- Insertar la verificación en la revisión de código y en la integración continua con un coste asumible.
El scanner: barato, valioso y estructuralmente limitado
El Accessibility Scanner de Google es una aplicación que se instala en el dispositivo, se activa como servicio y analiza la pantalla actual bajo demanda, señalando sobre una captura los elementos problemáticos. Detecta con fiabilidad una familia concreta de defectos: descripciones ausentes, descripciones duplicadas entre elementos hermanos, objetivos táctiles por debajo del mínimo, relaciones de contraste insuficientes y elementos superpuestos que se estorban.
Es la herramienta de mayor rendimiento por minuto invertido que existe en este terreno, y a la vez conviene entender con precisión su techo, porque la falsa sensación de conformidad es peor que la ausencia de herramienta.
Lo que sí encuentra
Ausencias y magnitudes. Todo lo que se puede decidir mirando un nodo aislado y sus números: hay descripción o no la hay, el rectángulo mide o no mide, los dos colores contrastan o no.
Lo que no puede encontrar
Todo lo que exige entender el contenido: si la descripción es correcta, si la agrupación respeta la estructura, si el orden tiene sentido, si el estado se anuncia al cambiar, si la locución resultante es comprensible.
La asimetría es sistemática y no se resolverá con mejores heurísticas: las herramientas automáticas verifican propiedades sintácticas, y la accesibilidad real es un problema semántico. Un botón etiquetado como boton uno pasa el scanner sin una sola advertencia y es completamente inútil. Por eso el scanner debe entenderse como un filtro de suelo —garantiza que no hay defectos groseros— y jamás como una certificación.
Las estimaciones más citadas en la literatura sitúan entre el veinte y el treinta por ciento la fracción de defectos de accesibilidad que las herramientas automáticas pueden detectar. El dato importa menos por su precisión que por su orden de magnitud: aunque tu informe salga completamente limpio, has cubierto una cuarta parte del problema. Presentar ese informe como evidencia de conformidad, cosa que ocurre a menudo cuando la accesibilidad se gestiona como cumplimiento normativo, es un error de método antes que de intención.
También conviene interpretar sus falsos positivos con criterio en lugar de suprimirlos por reflejo. El scanner señala descripciones duplicadas entre hermanos porque suele ser síntoma de etiquetas genéricas, pero en una tabla de datos la repetición puede ser correcta. Señala objetivos pequeños incluso cuando el elemento no es interactivo. Cada aviso descartado debería dejar una anotación con el motivo, porque el mismo aviso volverá a aparecer dentro de tres meses ante otra persona.
Diez minutos recorriendo tu flujo principal con TalkBack encendido y la pantalla apagada encuentran más defectos reales que cualquier informe automático, porque someten la interfaz a la única prueba que importa: ¿se puede completar la tarea escuchando? Hazlo con el gesto de deslizamiento, sin exploración táctil, para experimentar el recorrido secuencial tal como lo vive quien no sabe dónde están los elementos.
Fijar el contrato con asertos semánticos
Los tests de Compose operan sobre el árbol de semántica, lo que significa que ya tienes la infraestructura necesaria: cualquier aserto sobre un nodo es, implícitamente, un aserto sobre accesibilidad. La diferencia entre un test funcional y un test de accesibilidad no está en la maquinaria sino en qué decides afirmar.
@Test fun laTarjetaSeLeeComoUnaUnidad() {
rule.setContent { TarjetaDeProducto(producto = ejemplo) }
rule.onNodeWithContentDescription("Auriculares Modelo X, 89 euros")
.assertExists()
.assertHasClickAction()
// ningun descendiente debe ser parada independiente
rule.onAllNodesWithText("89 euros", useUnmergedTree = false)
.assertCountEquals(0)
}
Ese par de asertos codifica una decisión de diseño que hasta ahora solo vivía en la cabeza de quien la tomó: esta tarjeta es una unidad. Si mañana alguien retira la fusión al refactorizar, el test falla y explica por qué. Es exactamente el mismo mecanismo con el que protegemos cualquier invariante, aplicado a un invariante que normalmente nadie escribe.
Conviene notar que estos tests son unitarios de composición, no instrumentados pesados: corren en cuestión de milisegundos, no necesitan un dispositivo con servicios de accesibilidad activos y encajan en la misma tarea de Gradle que el resto de tus pruebas de interfaz. Su coste marginal es prácticamente nulo, lo que elimina el argumento habitual contra ellos.
Los asertos más rentables son cuatro. Que un elemento crítico tenga descripción no vacía. Que un control anuncie su estado y que ese anuncio cambie al interactuar. Que un contenedor produzca una única parada. Y que un elemento decorativo esté ausente del árbol.
Hay además un quinto tipo de comprobación, más ambiciosa y muy poco usada, que consiste en operar la pantalla exclusivamente a través de las acciones semánticas en lugar de simular gestos. Un test que completa el flujo de compra invocando performSemanticsAction en cada paso demuestra algo que ningún aserto aislado demuestra: que la tarea entera es realizable sin ver la pantalla. Es la traducción a código del recorrido manual con lector, y aunque no lo sustituye, protege el camino crítico de regresiones silenciosas.
@Test fun elInterruptorAnunciaSuEstado() {
rule.setContent { FilaDeNotificaciones() }
rule.onNodeWithText("Notificaciones")
.assert(SemanticsMatcher.expectValue(
SemanticsProperties.StateDescription, "Silenciado"))
.performClick()
rule.onNodeWithText("Notificaciones")
.assert(SemanticsMatcher.expectValue(
SemanticsProperties.StateDescription, "Activo"))
}
Validación automática dentro de los tests existentes
Además de los asertos que tú escribes, el marco de pruebas puede ejecutar automáticamente las mismas comprobaciones del scanner sobre cada acción de cada test instrumentado. Se habilita una vez y a partir de ahí toda la suite existente empieza a verificar contraste, tamaños de objetivo y descripciones sin que nadie escriba una línea nueva.
@Before fun activarChequeos() {
AccessibilityChecks.enable()
.setRunChecksFromRootView(true)
.setSuppressingResultMatcher(
matchesViews(withId(R.id.legacy_banner)))
}
El resultado de cada comprobación llega con severidad, con el nodo implicado y con una explicación, y puede configurarse para fallar el test o para limitarse a registrar el hallazgo. Empezar en modo informativo durante un par de semanas, medir el volumen y solo entonces endurecerlo es la secuencia que evita la revuelta del equipo.
El valor de este mecanismo es de escala: multiplica la cobertura por la cantidad de tests que ya tienes. Su peligro es el mismo que el de cualquier verificación heredada que empieza a fallar de golpe. Si al activarla revienta media suite, la tentación de suprimirlo todo es enorme y el resultado sería un cheque en blanco permanente. La estrategia sana es la contraria: activar la validación solo para los módulos nuevos o para las pantallas ya saneadas, y ampliar el perímetro pantalla a pantalla, dejando registrada cada supresión con su motivo y su fecha de caducidad.
flowchart TD A[Cambio en una rama] --> B[Tests unitarios de semantica] B --> C[Validacion automatica en instrumentados] C --> D[Revision de codigo con lista de comprobacion] D --> E[Scanner manual sobre pantallas nuevas] E --> F[Recorrido con lector de pantalla por trimestre] F --> G[Merge] B -.-> H[Fallo temprano y barato] C -.-> H
Meterlo en el proceso o perderlo todo
La verificación técnica solo se sostiene si el proceso la exige, y el proceso solo la exige si el coste marginal es bajo. Cuatro medidas concretas, ordenadas por relación entre esfuerzo y efecto.
La primera es una lista de comprobación breve en la plantilla de pull request, no más de cinco puntos: los elementos nuevos tienen descripción o están marcados como decorativos, los objetivos táctiles llegan al mínimo, los contenedores informativos fusionan, el estado se anuncia, y se probó con la fuente al máximo. Cinco casillas que el autor marca y el revisor comprueba.
La segunda es la integración continua. Los tests de semántica corren donde ya corren los demás y no requieren infraestructura adicional. Que un pull request no pueda fusionarse con un aserto de accesibilidad roto es lo que convierte la intención en garantía. A esa red conviene añadirle una regla de análisis estático: el propio lint de Android detecta descripciones ausentes en recursos y componentes clásicos, y elevar esas reglas de aviso a error en la configuración del módulo cierra por arriba una familia entera de defectos sin escribir un solo test.
android {
lint {
error += setOf("ContentDescription", "ClickableViewAccessibility")
abortOnError = true
}
}
La tercera es la definición de terminado. Si accesibilidad no forma parte de los criterios de aceptación de una historia, se convierte en trabajo opcional, y el trabajo opcional se pospone indefinidamente bajo presión de entrega. Conviene además que las revisiones de diseño incluyan contraste y jerarquía, porque un contraste insuficiente aprobado en la maqueta llega al código como requisito.
La cuarta es el recorrido periódico con lector de pantalla, en rotación entre los miembros del equipo. Su función no es solo encontrar defectos: es que la experiencia de usar la propia aplicación sin ver deje de ser abstracta para quien la construye.
Una advertencia final sobre la estrategia de adopción, porque es donde fracasan la mayoría de las iniciativas. El impulso natural ante una base de código grande y desatendida es lanzar una campaña de saneamiento general, que consume semanas, produce un pico de mejora y no deja ningún mecanismo detrás. El enfoque que sí funciona es el opuesto y se resume en dos reglas: todo lo nuevo nace conforme, y lo viejo se sanea cuando se toca. Ese régimen no arregla la aplicación en un trimestre, pero garantiza que la deuda deja de crecer desde el primer día y que las zonas más activas —que son también las más usadas— se reparan solas con el tiempo.
Y por encima de cualquier técnica está la única fuente de verdad que no admite sustituto: las personas que dependen de estas tecnologías a diario. Ninguna auditoría, ningún test y ninguna simulación equivale a media hora observando a un usuario real de lector de pantalla intentar completar una tarea en tu aplicación. Si tu organización tiene presupuesto para una sola cosa de esta lección, que sea esa.
Merece la pena cerrar el nivel con la observación que gobierna todo lo anterior y que trasciende con mucho el asunto de la accesibilidad. Cualquier propiedad de un sistema que no esté respaldada por un mecanismo de verificación automático es, a plazo suficientemente largo, una propiedad que el sistema no tiene. No porque los equipos sean descuidados, sino por pura entropía: un sistema recibe miles de modificaciones de docenas de personas con contextos parciales, y las propiedades que solo viven en la memoria de quien las implantó desaparecen con la primera rotación de plantilla. La accesibilidad es un caso extremo de este fenómeno por una razón específica y perversa: sus defectos son invisibles para quien los introduce. Un fallo de rendimiento se nota, un fallo funcional se reporta, un fallo visual se ve en la captura del pull request; un contentDescription perdido no produce absolutamente ninguna señal para un desarrollador vidente que ejecuta la aplicación en su escritorio. El defecto viaja hasta producción a través de todos los filtros habituales sin activar ninguno, y allí no genera un informe de error sino un abandono silencioso, que es la forma de fallo que ningún panel de métricas sabe leer. De ahí se sigue el corolario práctico: la única defensa proporcionada a esa invisibilidad es la automatización, y la única automatización útil es la que corre en cada cambio. Pero hay una segunda mitad del argumento, y es la que separa a los equipos que lo hacen bien de los que solo lo parecen. Una verificación automática que solo comprueba lo sintáctico —hay etiqueta, mide bastante, contrasta lo suficiente— genera una métrica verde que puede convivir perfectamente con una aplicación imposible de usar. Un panel al cien por cien de conformidad y un usuario que no logra comprar un billete son un estado del mundo completamente coherente. Por eso el andamiaje tiene que ser de dos capas, y las dos son obligatorias: la automatización sostiene el suelo, e impide que se pierda lo ganado; el recorrido humano periódico, con el lector encendido y la pantalla apagada, es lo único que verifica el techo. Renunciar a la primera condena el trabajo a evaporarse. Renunciar a la segunda produce algo más peligroso todavía, que es la certeza documentada de estar haciendo bien algo que no funciona.
- Pasa el
Accessibility Scannerpor tus cinco pantallas principales y clasifica cada hallazgo en real, falso positivo o síntoma de un problema más profundo. - Escribe tres asertos semánticos que fijen decisiones de agrupación que hoy solo existen en tu cabeza y comprueba que fallan si retiras la fusión.
- Activa la validación automática en un módulo ya saneado, registra cada supresión necesaria con su motivo y fija una fecha para revisarlas.
- Añade cinco casillas de accesibilidad a la plantilla de pull request y aplícala durante dos semanas, midiendo cuántos defectos captura antes del
merge. - Recorre el flujo crítico con
TalkBacky la pantalla apagada, cronometra la tarea y compárala con el tiempo que tardas mirando.