Vimos no último post sobre o Watir, como melhorar os nossos testes usando o Rspec.
Já nesse post, iremos vê como melhorar os testes que escrevemos, utilizando um helper criado pelo Jeff Morgan.
Mas antes disso, deixa eu contar uma breve história sobre os nossos testes.
O Twitter mudou e quebrou os nossos testes
Como vocês devem ter notado, o Twitter recentemente (coisa de 1/2 meses), mudou a página de login. E a alteração também afetou os ids de alguns elementos da página, como por exemplo, o do text field do login e senha.
Os posts já foram atualizados, mas o que ocorreu é interessante pra ilustrar a fragilidade dos testes que dependem da interface.
Pode parecer estranho, mas tanto testar como desenvolver front-end são tarefas complicadas, principalmente Web (só lembrar das várias tecnologias usadas numa interface Web e os vários navegadores existentes). A sensação e de que quando falamos de sistemas front-end, pisamos num chão mais gelatinoso, e qualquer mudança faz ele tremer.
Para evitar o que ocorreu, o ideal é a equipe de desenvolvimento NÃO MUDAR o id dos elementos, afinal de contas, para o usuário tanto faz se o text field tem o id username ou login, mas para os Testadores isso importa, e muito. Aliás, para ajudar na nomeação desses ids, estabelecer um padrão pra equipe ou boas práticas pode ajudar.
Bem era isso, agora vamos falar do helper que irá melhorar os nossos testes.
Melhorando os testes
Esse helper é uma mão na roda pra quem utiliza o Watir, com ele vamos passar a escrever menos código e será mais fácil de manter os nossos testes.
O helper mágico que estou falando é o seguinte:
module WatirHelper # A helper class to make accessing web elements via Watir easier. # All methods take an identifier parameter. This parameter is # an array of hashes that are used to identify an element on the # page. On elements that support multiple attributes you can # provide multiple identifiers. # # This module assumes there is a @browser variable available. def self.included(cls) cls.extend ClassMethods end module ClassMethods # adds three methods - one to put data in a text field, another # to fetch that data, and another to return the actual text_field. # # Example: text_field(:first_name, {:id => "first_name}) # will generate the 'first_name', 'first_name=', and # 'first_text_field' methods def text_field(name, identifier) define_method(name) do @browser.text_field(identifier).value end define_method("#{name}=") do |_value| @browser.text_field(identifier).value = _value end define_method("#{name}_text_field") do @browser.text_field(identifier) end end # adds three methods - one to put data in a hidden field, another # to fetch that data, and a third to return the hidden field. # # Example: hidden(:first_name, {:id => "first_name}) # will generate the 'first_name', 'first_name=' and # 'first_hidden' methods def hidden(name, identifier) define_method(name) do @browser.hidden(identifier).value end define_method("#{name}=") do |value| @browser.hidden(identifier).set(value) end define_method("#{name}_hidden") do @browser.hidden(identifier) end end # adds three methods - one to select an item in a drop-down, # another to fetch the currently selected item, and another # to return the select_list. # # Example: select_list(:state, {:id => "state"}) # will generate the 'state', 'state=' and 'state_select_list' # methods def select_list(name, identifier) define_method(name) do @browser.select_list(identifier).value end define_method("#{name}=") do |value| @browser.select_list(identifier).set(value) end define_method("#{name}_select_list") do @browser.select_list(identifier) end end # adds three methods - one to check, one to uncheck and # a third to return a checkbox # # Example: checkbox(:active, {:name => "is_active"}) # will generate the 'check_active', 'uncheck_active', and # 'active_checkbox' methods def checkbox(name, identifier) define_method("check_#{name}") do @browser.checkbox(identifier).set end define_method("uncheck_#{name}") do @browser.checkbox(identifier).clear end define_method("#{name}_checkbox") do @browser.checkbox(identifier) end end # adds three methods - one to select, another to clear and # another to return a radio button # # Example: radio_button(:north, {:id => "north"}) # will generate 'select_north', 'clear_north', and # 'north_radio_button' methods def radio_button(name, identifier) define_method("select_#{name}") do @browser.radio(identifier).set end define_method("clear_#{name}") do @browser.radio(identifier).clear end define_method("#{name}_radio_button") do @browser.radio(identifier) end end # adds three methods - one click a button, another # to click a button without waiting for the action to # complete, and a third to return the button. # # Example: button(:save, {:value => "save"}) # will generate the 'save', 'save_no_wait', and # 'save_button' methods def button(name, identifier) define_method(name) do @browser.button(identifier).click end define_method("#{name}_no_wait") do @browser.button(identifier).click_no_wait end define_method("#{name}_button") do @browser.button(identifier) end end # adds three methods - one to select a link, another # to select a link and not wait for the corresponding # action to complete, and a third to return the link. # # Example: link(:add_to_cart, {:text => "Add to Cart"}) # will generate the 'add_to_cart', 'add_to_cart_no_wait', # and 'add_to_cart_link' methods def link(name, identifier) define_method(name) do @browser.link(identifier).click end define_method("#{name}_no_wait") do @browser.link(identifier).click_no_wait end define_method("#{name}_link") do @browser.link(identifier) end end # adds a method that returns a table element # # Example: table(:shopping_cart, {:index => 1}) # will generate a 'shopping_cart' method def table(name, identifier) define_method(name) do @browser.table(identifier) end end # adds two methods - one to return the text within # a row and one to return a table row element # # Example: row(:header, {:id => :header}) will # generate a 'header' and 'header_row' method def row(name, identifier) define_method(name) do @browser.row(identifier).text end define_method("#{name}_row") do @browser.row(identifier) end end # adds a method to return the text of a table data <td> element # and another one to return the cell object # # Example: cell(:total, {:id => "total"}) # will generate a 'total' method and a 'total_cell' # method def cell(name, identifier) define_method(name) do @browser.cell(identifier).text end define_method("#{name}_cell") do @browser.cell(identifier) end end # adds a method that returns the content of a <div> # and another method that returns the div element # # Example: div(:header, {:id => "banner"}) # will generate a 'header' and 'header_div' methods def div(name, identifier) define_method(name) do @browser.div(identifier).text end define_method("#{name}_div") do @browser.div(identifier) end end # adds a method that returns the content of a <dd> # and another method that returns the dd element def dd(name, identifier) define_method(name) do @browser.dd(identifier).text end define_method("#{name}_dd") do @browser.dd(identifier) end end # adds a method that returns the content of a <dl> # and another that returns the dl element def dl(name, identifier) define_method(name) do @browser.dl(identifier).text end define_method("#{name}_dl") do @browser.dl(identifier) end end # adds a method that returns the content of a <dt> # and another that returns the dt element def dt(name, identifier) define_method(name) do @browser.dt(identifier).text end define_method("#{name}_dt") do @browser.dt(identifier) end end # adds a method that returns the content of a # <form> element and another that returns the # form element def form(name, identifier) define_method(name) do @browser.form(identifier).text end define_method("#{name}_form") do @browser.form(identifier) end end # adds a method that returns a the content of a # <frame> element and another that returns the # frame element def frame(name, identifier) define_method(name) do @browser.frame(identifier).text end define_method("#{name}_frame") do @browser.frame(identifier) end end # adds a method that returns an image <image> element def image(name, identifier) define_method(name) do @browser.image(identifier) end end end def content @browser.text end def visit_page(page_url) @browser.goto(page_url) end def page_title @browser.title end def wait_for_page @browser.wait end end
Os comentários feitos pelo Jeff já explicam muito bem o que esse helper faz. Mas mesmo assim deixa eu dá uma explicação rápida, sobre a magia por de trás dele: Basicamente ele facilita a definição dos elementos da página. Provendo tanto o setter quanto getter dos elementos que iremos interagir durante o teste, além de prover o objeto do próprio elemento.
Pra utilizar esse helper, basta fazer o include, o que irá fazer com que a classe HomePage “ganhe” todos os métodos dele, como por exemplo o método page_title, que irá retornar o título da página que está aberta.
Abaixo está como ficou o código dos nossos testes com a utilização desse helper.
require 'rubygems' #require 'watir' require 'firewatir' require 'lib/watir_helper.rb' module Twitter class HomePage include WatirHelper HOME_PAGE = 'twitter.com' link(:tweet_button, :class => 'tweet-button button') link(:tweet_button_disable, :class => 'tweet-button button disabled') text_field(:username, :name => 'session[username_or_email]') text_field(:password, :name => 'session[password]') text_field(:editor, :class => 'twitter-anywhere-tweet-box-editor') button(:sign_in_submit, :class => 'submit button') div(:message, :class => 'tweet-text') def initialize @browser = Watir::Browser.new end def visit @browser.goto(HOME_PAGE) end def login(username, password) self.username = username self.password = password self.sign_in_submit end def type_message(message) self.editor = message self.editor_text_field.fire_event('onMouseDown') end def tweet self.tweet_button end def message_exists?(message) @browser.wait_until {self.message_div.text == message} end def alert_message_exists?(message) @browser.wait_until {@browser.text.include? message} end def tweet_button_is_disabled? @browser.link(self.tweet_button_disable_link.exists?) end end end
O que ganhamos utilizando esse helper, você pode estar se perguntando. Basicamente duas vantagens.
Definição mais simples dos elementos web
Ganhamos três métodos pra cada elemento:
- um pra setar o valor do elemento (setter): self.username =
- outro pra retornar o valor do elemento (getter): self.username
- e por fim um que retorna o próprio objeto do elemento: username_text_field (podemos utilizar qualquer método do objeto TextField)
- o NOME_DO_SEU_ELEMENTO_text_field é para o caso do elemento ser da classe TextField. Se for um div, seria _div, se fosse um button seria _button e assim vai.
Manutenção mais controlada
Como você viu no começo do post, não é difícil a gente precisar dá manutenção nos nossos testes, e definindo desta forma os elementos que utilizamos, precisamos alterar apenas em um lugar eles.
Por exemplo, se a classe do div com o tweet mudou de tweet-text para apenas tweet, basta mudar a definição desse div, ficando assim: div(:message, :class => ‘tweet’)
Ambas vantagens são mais evidentes ainda, quando temos suítes maiores de testes.
Qualquer dúvida a respeito do uso do helper ou do próprio Watir, abuse e use dos comentários. Lembrando que o helper, foi disponibilizado pelo Jeff Morgan no seu GitHub.
O projeto completo deste post, está disponível no meu GitHub. Aliás, você pode vê a evolução do projeto navegando pelas tags do projeto.
Abraços!