Advertisement
  1. Code
  2. Ruby

Objetos de página de Ruby para conocedores de Capybara

Scroll to top
Read Time: 15 min

Spanish (Español) translation by Andrea Jiménez (you can also view the original English article)

Final product imageFinal product imageFinal product image
What You'll Be Creating

¿Qué son los objetos de página?

Primero te daré la presentación breve. Es un patrón de diseño para encapsular el marcado y las interacciones de la página, específicamente para refactorizar las especificaciones de tus funciones. Es una combinación de dos técnicas de refactorización muy comunes: la clase de extracción y el método de extracción, que no tienen por qué suceder al mismo tiempo porque puedes ir acumulando gradualmente hasta extraer una clase completa a través de un nuevo objeto de página.

Esta técnica te permite escribir especificaciones de características de alto nivel que son muy expresivas y SECAS. En cierto modo, son pruebas de aceptación con lenguaje de aplicación. Podrías preguntar, ¿no están las especificaciones escritas con Capybara ya de alto nivel y expresivas? Claro, para los desarrolladores que escriben código a diario, las especificaciones de Capybara se leen bien. ¿Están SECAS por defecto? No realmente, de hecho, ¡ciertamente no!

1
feature 'M creates a mission' do
2
  scenario 'successfully' do
3
    sign_in_as 'M@mi6.com'
4
    
5
    visit missions_path
6
    click_on     'Create Mission' 
7
    fill_in      'Mission Name',  with: 'Project Moonraker'
8
    click_button 'Submit'
9
10
    expect(page).to have_css 'li.mission-name', text: 'Project Moonraker'
11
  end
12
end
1
feature 'M marks mission as complete' do
2
  scenario 'successfully' do
3
    sign_in_as 'M@mi6.com'
4
    
5
    visit missions_path
6
    click_on     'Create Mission' 
7
    fill_in      'Mission Name', with: 'Octopussy'
8
    click_button 'Submit'
9
10
    within "li:contains('Octopussy')" do
11
      click_on 'Mission completed'
12
    end
13
14
    expect(page).to have_css 'ul.missions li.mission-name.completed', text: 'Octopussy'
15
  end
16
end

Cuando observas estos ejemplos de especificaciones de características, ¿dónde ves oportunidades para mejorar esta lectura y cómo podría extraer información para evitar la duplicación? Además, ¿es esto suficiente para un modelado fácil de las historias de usuario y para que las partes interesadas no técnicas lo entiendan?

En mi opinión, hay un par de formas de mejorar esto y hacerlos felices a todos: desarrolladores que pueden evitar jugar con los detalles de interactuar con el DOM mientras aplican OOP, y otros miembros del equipo que no son de codificación que no tienen problemas para saltar entre las historias de usuario y estas pruebas. Es bueno tener el último punto, sin duda, pero los beneficios más importantes provienen principalmente de hacer que tus especificaciones de interacción DOM sean más sólidas.

La encapsulación es el concepto clave con los objetos de página. Cuando escribas las especificaciones de tus funciones, te beneficiarás de una estrategia para extraer el comportamiento que está impulsando un flujo de prueba. Para el código de calidad, quieres capturar las interacciones con conjuntos particulares de elementos en tus páginas, especialmente si tropiezas con patrones repetidos. A medida que tu aplicación crece, quieres / necesitas un enfoque que evite difundir esa lógica en todas tus especificaciones.

"Bueno, ¿no es eso exagerado? ¡Capybara lee bien!", ¿tú dices?

Pregúntate: ¿Por qué no tendrías todos los detalles de implementación de HTML en un solo lugar mientras tiene pruebas más estables? ¿Por qué las pruebas de interacción de la interfaz de usuario no deberían tener la misma calidad que las pruebas para el código de la aplicación? ¿Realmente quieres detenerte allí?

Debido a los cambios diarios, tu código Capybara es vulnerable cuando se extiende por todas partes: introduce posibles puntos de interrupción. Digamos que un diseñador quiere cambiar el texto de un botón. No es gran cosa, ¿verdad? Pero, ¿quieres adaptarte a ese cambio en un contenedor central para ese elemento en tus especificaciones, o prefieres hacerlo por todas partes? ¡Ya me lo imaginaba!

Hay muchas refactorizaciones posibles para tus especificaciones de características, pero los Objetos de página ofrecen las abstracciones más limpias para encapsular el comportamiento orientado al usuario para páginas o flujos más complejos. Sin embargo, no tienes que simular la(s) página(s) completa(s); céntrate en los bits esenciales que son necesarios para los flujos de usuarios. ¡No hay necesidad de exagerar!

Pruebas de aceptación / Especificaciones de características

Antes de pasar al meollo del asunto, me gustaría dar un paso atrás para las personas nuevas en todo el negocio de las pruebas y aclarar algo de la jerga que es importante en este contexto. Las personas más familiarizadas con TDD no se perderán mucho si se adelantan.

¿De qué estamos hablando aqui? Las pruebas de aceptación generalmente se realizan en una etapa posterior de los proyectos para evaluar si has estado construyendo algo de valor para tus usuarios, propietario de producto u otra parte interesada. Estas pruebas suelen ser realizadas por clientes o usuarios. Es una especie de verificación de si los requisitos se están cumpliendo o no. Hay algo así como una pirámide para todo tipo de capas de prueba, y las pruebas de aceptación están cerca de la cima. Debido a que este proceso a menudo incluye personas sin conocimientos técnicos, un lenguaje de alto nivel para escribir estas pruebas es un activo valioso para comunicarse de un lado a otro.

Las especificaciones de características, por otro lado, son un poco más bajas en la cadena alimentaria de pruebas. Mucho más de alto nivel que las pruebas unitarias, que se centran en los detalles técnicos y la lógica empresarial de tus modelos, las especificaciones de funciones describen los flujos en tus páginas y entre ellas.

Herramientas como Capybara te ayudan a evitar hacer esto manualmente, lo que significa que rara vez tienes que abrir tu navegador para probar cosas manualmente. Con este tipo de pruebas, nos gusta automatizar estas tareas tanto como sea posible y probar la interacción a través del navegador mientras escribimos afirmaciones en las páginas. Por cierto, no usas get, put, post o delete como lo haces con las especificaciones de solicitud.

Las especificaciones de las características son muy similares a las pruebas de aceptación; a veces siento que las diferencias son demasiado confusas para preocuparme realmente por la terminología. Escribes pruebas que ejercitan toda tu aplicación, lo que a menudo implica un flujo de acciones de usuario de varios pasos. Estas pruebas de interacción muestran si tus componentes funcionan en armonía cuando se unen.

En La tierra de Ruby, son los principales protagonistas cuando se trata de objetos de página. Las especificaciones de características en sí mismas ya son muy expresivas, pero se pueden optimizar y limpiar extrayendo sus datos, comportamiento y marcado en una clase o clases separadas.

Espero que aclarar esta terminología confusa te ayude a ver que tener Page Objects es un poco como hacer pruebas de nivel de aceptación mientras escribes especificaciones de funciones.

Capybara

Tal vez deberíamos repasar esto muy rápidamente también. Esta biblioteca se describe a sí misma como un "framework de prueba de aceptación para aplicaciones web". Puedes simular las interacciones del usuario con tus páginas a través de un lenguaje específico de dominio muy potente y conveniente. En mi opinión personal, RSpec junto con Capybara ofrece la mejor manera de escribir tus especificaciones de características en este momento. Te permite visitar páginas, completar formularios, hacer clic en enlaces y botones, y buscar marcas en tus páginas, y puedes combinar fácilmente todo tipo de estos comandos para interactuar con tus páginas a través de tus pruebas.

Básicamente, puedes evitar abrir el navegador tú mismo para probar estas cosas manualmente la mayor parte del tiempo, lo que no solo es menos elegante, sino que también consume mucho más tiempo y es propenso a errores. Sin esta herramienta, el proceso de "pruebas externas" (conduces tu código de pruebas de alto nivel a tus pruebas de nivel unitario) sería mucho más doloroso y, posiblemente, por lo tanto, más descuidado.

En otras palabras, comienzas a escribir estas pruebas de características que se basan en tus historias de usuario, y desde allí vas por la madriguera del conejo hasta que tus pruebas unitarias proporcionen la cobertura que tus especificaciones de características necesitaban. Después de eso, cuando tus pruebas están en verde, por supuesto, el juego comienza de nuevo y vuelves a subir para continuar con una nueva prueba de características.

Veamos dos ejemplos simples de especificaciones de características que le permiten a M crear misiones clasificadas que luego se pueden completar.

En el marcado, tienes una lista de misiones, y una finalización exitosa crea una clase adicional completed en el li de esa misión en particular. Cosas sencillas, ¿verdad? Como primer enfoque, comencé con pequeñas refactorizaciones muy comunes que extraen el comportamiento común en los métodos.

spec/features/m_creates_a_mission_spec.rb

1
require 'rails_helper'
2
3
feature 'M creates mission' do
4
  scenario 'successfully' do
5
    sign_in_as 'M@mi6.com'
6
    
7
    create_classified_mission_named 'Project Moonraker'
8
		
9
    agent_sees_mission              'Project Moonraker'
10
  end
11
12
  def create_classified_mission_named(mission_name)
13
    visit missions_path
14
    click_on     'Create Mission' 
15
    fill_in      'Mission Name', with: mission_name
16
    click_button 'Submit'
17
  end
18
19
  def agent_sees_mission(mission_name)
20
    expect(page).to have_css 'li.mission-name', text: mission_name
21
  end
22
23
  def sign_in_as(email)
24
    visit root_path
25
    fill_in      'Email', with: email
26
    click_button 'Submit'
27
  end
28
end

spec/features/agent_completes_a_mission_spec.rb

1
require 'rails_helper'
2
3
feature 'M marks mission as complete' do
4
  scenario 'successfully' do
5
    sign_in_as 'M@mi6.com'
6
    
7
    create_classified_mission_named 'Project Moonraker'
8
    mark_mission_as_complete        'Project Moonraker'
9
10
    agent_sees_completed_mission    'Project Moonraker'
11
  end
12
13
  def create_classified_mission_named(mission_name)
14
    visit missions_path
15
    click_on     'Create Mission' 
16
    fill_in      'Mission Name', with: mission_name
17
    click_button 'Submit'
18
  end
19
20
	def mark_mission_as_complete(mission_name)
21
    within "li:contains('#{mission_name}')" do
22
      click_on 'Mission completed'
23
    end
24
  end
25
26
  def agent_sees_completed_mission(mission_name)
27
    expect(page).to have_css 'ul.missions li.mission-name.completed', text: mission_name
28
  end
29
30
  def sign_in_as(email)
31
    visit root_path
32
    fill_in      'Email', with: email
33
    click_button 'Submit'
34
  end
35
end

Aunque hay otras formas, por supuesto, de lidiar con cosas como sign_in_as, create_classified_mission_named, entre otros, es fácil ver qué tan rápido estas cosas pueden comenzar a ser malas y acumularse.

Las especificaciones relacionadas con la interfaz de usuario a menudo no reciben el tratamiento OO que necesitan / merecen, creo. Tienen la reputación de proporcionar muy poco por el dinero y, por supuesto, a los desarrolladores no les gustan mucho los momentos en que tienen que tocar mucho las cosas de marcado. En mi opinión, eso hace que sea aún más importante SECAR estas especificaciones y hacer que sea divertido lidiar con ellas lanzando un par de clases de Ruby.

Hagamos un pequeño truco de magia en el que oculto la implementación del objeto de página por ahora y solo te muestro el resultado final aplicado a las especificaciones de funciones anteriores:

spec/features/m_creates_a_mission_spec.rb

1
require 'rails_helper'
2
3
feature 'M creates mission' do
4
  scenario 'successfully' do
5
    sign_in_as 'M@mi6.com'
6
    visit missions_path
7
    mission_page = Pages::Missions.new
8
		
9
    mission_page.create_classified_mission_named 'Project Moonraker'
10
11
    expect(mission_page).to have_mission_named   'Project Moonraker'
12
  end
13
end

spec/features/agent_completes_a_mission_spec.rb

1
require 'rails_helper'
2
3
feature 'M marks mission as complete' do
4
  scenario 'successfully' do
5
    sign_in_as 'M@mi6.com'
6
    visit missions_path
7
    mission_page = Pages::Missions.new
8
9
    mission_page.create_classified_mission_named         'Project Moonraker'
10
    mission_page.mark_mission_as_complete                'Project Moonraker'
11
    
12
    expect(mission_page).to have_completed_mission_named 'Project Moonraker'
13
  end
14
end

No lee tan mal, ¿eh? Básicamente, crea métodos de envoltura expresivos en tus objetos de página que te permiten lidiar con conceptos de alto nivel, en lugar de jugar en todas partes con los intestinos de tu marcado todo el tiempo. Tus métodos extraídos hacen este tipo de trabajo sucio ahora, y de esa manera la cirugía de escopeta ya no es tu problema.

Dicho de otra manera, encapsulas la mayor parte del ruidoso código de interacción DOM. Sin embargo, tengo que decir que a veces los métodos extraídos de forma inteligente en tus especificaciones de características son suficientes y se leen un poco mejor, ya que puedes evitar tratar con instancias de objetos de página. De todos modos, revisemos la implementación:

specs/support/features/pages/missions.rb

1
module Pages
2
  class Missions
3
    include Capybara::DSL
4
5
    def create_classified_mission_named(mission_name)
6
      click_on     'Create Mission'
7
      fill_in      'Mission name', with: mission_name
8
      click_button 'Submit'
9
    end
10
11
    def mark_mission_as_complete(mission_name)
12
      within "li:contains('#{mission_name}')" do
13
        click_on 'Mission completed'
14
      end
15
    end
16
17
    def has_mission_named?(mission_name)
18
      mission_list.has_css? 'li', text: mission_name
19
    end
20
21
    def has_completed_mission_named?(mission_name)
22
      mission_list.has_css? 'li.mission-name.completed', text: mission_name
23
    end
24
25
    private
26
27
    def mission_list
28
      find('ul.missions')
29
    end 
30
  end
31
end

Lo que ves es un objeto Ruby simple y antiguo: los objetos de página son, en esencia, clases muy simples. Normalmente, no creas instancias de objetos de página con datos (cuando se produce la necesidad, por supuesto, puedes hacerlo) y creas principalmente un lenguaje a través de la API que un usuario o una parte interesada no técnica de un equipo podría usar. Cuando piensas en nombrar tus métodos, creo que es un buen consejo hacerte la pregunta: ¿Cómo describiría un usuario el flujo o la acción realizada?

Tal vez debería agregar que sin incluir a Capybara, la música se detiene bastante rápido.

1
include Capybara::DSL

Probablemente te preguntes cómo funcionan estos emparejadores personalizados:

1
expect(page).to have_mission_named           'Project Moonraker'
2
expect(page).to have_completed_mission_named 'Project Moonraker'
3
4
def has_mission_named?(mission_name)
5
  ...
6
end
7
8
def has_completed_mission_named?(mission_name)
9
  ...
10
end

RSpec genera estos emparejadores personalizados basados en métodos de predicados en tus objetos de página. RSpec los convierte eliminando el ? y cambiando has a have. ¡Boom, matchers desde cero sin mucha confusión! Un poco de magia, te daré eso, pero el buen tipo de magia, diría yo.

Dado que estacionamos nuestro objeto de página en specs/support/features/pages/missions.rb, también debes asegurarte de que lo siguiente no se comente en spec/rails_helper.rb.

1
Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }

Si te encuentras con un NameError con una constante Pages no inicializada, sabrás qué hacer.

Si tienes curiosidad por saber qué pasó con el método sign_in_as, lo extraje en un módulo en spec/support/sign_in_helper.rb y le dije a RSpec que incluyera ese módulo. Esto no tiene nada que ver con los objetos de página directamente, simplemente tiene más sentido almacenar la funcionalidad de prueba, como sign in de una manera más accesible a nivel mundial que a través de un objeto de página.

spec/support/sign_in_helper.rb

1
module SignInHelper
2
  def sign_in_as(email)
3
    visit root_path
4
    fill_in      'Email', with: email
5
    click_button 'Submit'
6
	end
7
end

Y debes informar a RSpec que quieres acceder a este módulo auxiliar:

spec/spec_helper.rb

1
...
2
require 'support/sign_in_helper'
3
4
RSpec.configure do |config|
5
  config.include SignInHelper
6
	...
7
end

En general, es fácil ver que logramos ocultar los detalles de Capybara, como encontrar elementos, hacer clic en enlaces, entre otros. Ahora podemos centrarnos en la funcionalidad y menos en la estructura real del marcado, que ahora está encapsulada en un objeto de página: la estructura DOM debería ser la menor de tus preocupaciones cuando pruebes algo tan alto como las especificaciones de características.

¡Atención!

Los elementos de configuración, como los datos de fábrica, pertenecen a las especificaciones y no a los Objetos de página. Además, es probable que las afirmaciones estén mejor ubicadas fuera de tus objetos de la página para lograr una separación de preocupaciones.

Hay dos perspectivas diferentes sobre el tema. Los defensores de colocar afirmaciones en objetos de página dicen que ayuda a evitar la duplicación de afirmaciones. Puedes proporcionar mejores mensajes de error y lograr un mejor estilo "Di, no preguntes". Por otro lado, los defensores de los objetos de página sin afirmaciones argumentan que es mejor no mezclar responsabilidades. Proporcionar acceso a los datos de la página y la lógica de aserción son dos preocupaciones separadas y conducen a objetos de página hinchados cuando se mezclan. La responsabilidad de Page Object es el acceso al estado de las páginas, y la lógica de aserción pertenece a las especificaciones.

Tipos de objetos de página

Los componentes representan las unidades más pequeñas y están más enfocados, como un objeto de formulario, por ejemplo.

Las páginas combinan más de estos componentes y son abstracciones de una página completa.

Las experiencias, como ya habrás adivinado, abarcan todo el flujo a través de potencialmente muchas páginas diferentes. Son de más alto nivel. Se centran en el flujo que experimenta el usuario mientras interactúa con varias páginas. Un flujo de pago que tiene un par de pasos es un buen ejemplo para pensar en esto.

¿Cuándo y por qué?

Es una buena idea aplicar este patrón de diseño un poco más adelante en el ciclo de vida de un proyecto, cuando hayas acumulado un poco de complejidad en tus especificaciones de características y cuando puedas identificar patrones repetitivos como estructuras DOM, métodos extraídos u otros puntos en común que sean consistentes en tus páginas.

Por lo tanto, probablemente no deberías comenzar a escribir objetos de página de inmediato. Aborda estas refactorizaciones gradualmente cuando la complejidad y el tamaño de tu aplicación / pruebas crecen. Las duplicaciones y refactorizaciones que necesitan un mejor inicio a través de objetos de página serán más fáciles de detectar con el tiempo.

Mi recomendación es comenzar con la extracción de métodos en tus especificaciones de características localmente. Una vez que alcancen la masa crítica, parecerán candidatos obvios para una mayor extracción, y la mayoría de ellos probablemente se ajustarán al perfil de los Objetos de página. ¡Empieza con algo pequeño, porque la optimización prematura deja marcas desagradables!

Ideas finales

Los objetos de página te brindan la oportunidad de escribir especificaciones más claras que se leen mejor y, en general, son mucho más expresivas porque son más de alto nivel. Además de eso, ofrecen una buena abstracción para todos los que les gusta escribir código OO. Ocultan los detalles del DOM y también te permiten tener métodos privados que hacen el trabajo sucio sin estar expuestos a la API pública. Los métodos extraídos en tus especificaciones de funciones no ofrecen el mismo lujo. La API de Page Objects no necesita compartir los detalles esenciales de Capybara.

Para todos los escenarios en los que cambian las implementaciones de diseño, tus descripciones de cómo debería funcionar tu aplicación no necesitan cambiar cuando usas objetos de página: tus especificaciones de las características se centran más en las interacciones a nivel de usuario y no se preocupan tanto por los detalles de las implementaciones de DOM. Dado que el cambio es inevitable, los objetos de página se vuelven críticos cuando las aplicaciones crecen y también ayudan a comprender cuándo el gran tamaño de la aplicación significa una complejidad drásticamente mayor.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.