Escrevendo testes melhores com o Watir

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!

Melhorando os testes com o Rspec

No post anterior, utilizamos o excelente Watir (estou gostando mesmo dele), para testar um pouco o Twitter.

Neste iremos melhorar o código dos testes, como foi prometido.

Após a leitura do post, você estará sabendo:

  • O que é o Rspec;
  • Como escrever os seus testes com o Rspec;
  • Porquê utilizar o Rspec é uma boa ideia.

O que é o Rspec?

O Rspec é um framework de testes de unidade, assim como o JUnit. Porém ele vai além de apenas fornece um conjunto de métodos para os seus testes, e nos fornece uma DSL para descrever os compartamentos esperados do sistema[1], ou seja, ao invés, de ter apenas “um monte” de asserts, temos “um monte” de asserts dentro de exemplos executáveis, a diferença é sútil, mas faz um boa diferença.

Watir + Rspec = HAVEFUN

Os testes do último exemplo possuem um código bem ruim. Os problemas maiores dele são:

  • Repetição de código;
  • Testes fortemente acoplados;
  • Mal organizado.

Utilizando o Rspec podemos combater esses problemas. E lembre-se, não é porquê o código é para realizar testes, que ele pode ser feito de qualquer jeito, muito pelo contrário, devemos sim seguir boas práticas, como por exemplo os princípios SOLID.

Vamos agora direto vê como ficam os testes usando o Rspec:

require 'spec_helper'

USERNAME = 'YOUR USERNAME'
PASSWORD = 'YOUR PASSWORD'
MESSAGE = 'Watir rocks!'
MESSAGE_WITH_MORE_THAN_140_CHARACTERS =  "testing characters limit. 140 characters is the  characters limit. Are you pass? not yet? not? not? when are we pass? now? We break the limit!"
TWITTER_REPEATED_MESSAGE = 'Whoops! You already said that...'

describe Twitter::HomePage do

  before(:all) do
    @home_page = Twitter::HomePage.new
    @home_page.visit
    @home_page.login(USERNAME,PASSWORD)
  end

  it "send a message when it is valid" do
    @home_page.type_message(MESSAGE)
    @home_page.tweet
    @home_page.message_exists?(MESSAGE).should be_true
  end

  it "return a error messagem when try to send a repeated message" do
    @home_page.type_message(MESSAGE)
    @home_page.tweet
    @home_page.alert_message_exists?(TWITTER_REPEATED_MESSAGE).should be_true
  end

  it "don't allow to send a message when it contains more than 140 characters" do
    @home_page.type_message(MESSAGE_WITH_MORE_THAN_140_CHARACTERS)
    @home_page.tweet_button_is_disabled?.should be_true
  end

  it "don't allow to send a message when it is blank" do
    @home_page.type_message('')
    @home_page.tweet_button_is_disabled?.should be_true
  end

end

O que achou? Bem melhor que aquela “massaroca“, não é mesmo?

E viu só como o código ficou até mais simples. Prova que fazer da forma correta, é diferente de fazer da forma complicada.

O que mudou?

Antes tínhamos um código com muito repetição, é só olhar os vários ifs e elses para perceber que algo não estava certo. Agora usando o Rspec, temos os próprios métodos dele ao nosso dispor. E como você pode vê, o nosso código é praticamente um texto corrido.

Caso essa seja a primeira vez que você vê um código usando o Rspec, segue uma explicação abaixo sobre ele:

  • Primeiro estamos fazendo require de um helper, que apenas faz require da nossa classe HomePage (mais por organização);
  • No describe estamos passando a classe que iremos testar (Twitter é o módulo e HomePage é a classe) e um bloco (tudo dentro do do até o end lá do final é bloco que estamos passando);
  • O before contém passos que serão executados antes do teste (pré-condições), no nosso caso estamos logando no Twitter, antes de todos os testes. Ou seja, ele é executado apenas uma vez, e não antes de cada teste;
  • Depois temos os nossos exemplos/testes, cada um com uma asserção no final.

O que não mudou

Os nossos testes continuam com um bom nível de acoplamento/dependência, e isso costuma não ser bom. É só observar o segundo teste, ele depende do primeiro para funcionar, caso o primeiro teste falhe, o segundo provavelmente vai falar também.

No entanto, eu acabei optando por manter desta maneira, não por preguiça, mas sim porquê vejo que se o primeiro teste falha, o segundo deve falhar mesmo.

É mais uma escolha por deixar mais simples, embora eu goste da ideia de deixar os testes “atômicos”.

Organização e execução dos testes

Como você deve lembrar, temos a nossa classe HomePage, que ainda vai continuar da forma que está.

Continuamos usando ela, só que agora não temos tudo num mesmo arquivo. Temos agora um projeto, que segue a seguinte estrutura:

  • -> lib
    • -> twitter
      • home_page.rb
  • -> spec
    • -> twitter
      • home_page_spec.rb
    • spec_helper.rb

Resumindo: na pasta lib temos a classe HomePage, que prover os métodos para facilitar a criação e reutilização dos testes, que estão na pasta spec. Ambas as pastas twitter, existem pois podemos criar testes para outro site, e separar ele em outra pasta.

Por exemplo, se a gente for testar o Google, podemos criar uma pasta google, dentro de lib e spec. E aí na primeira ficarão as classes para os nossos testes e na segunda os testes em si. Tá vendo só como fica bem organizado, mesmo quando começarmos a ter muitos arquivos e pastas.

Passada a explicação sobre a organização do nosso projeto, vamos vê o díficil comando para rodar os nossos testes. Basta digitar: rspec spec/twitter/home_page_spec.rb

Só isso mesmo. Os testes serão executados e você verá em seu terminal/prompt o resultado dos testes.

Melhor que isso, só isso:

rspec spec/twitter/home_page_spec.rb -f h > resultado.html

Agora temos o resultado dos testes numa bela página em HTML. 🙂

Por que usar o Rspec?

Após mostrar um exemplo sem o Rspec e outro com, deve ter ficado bem clara a diferença das duas abordagens.

Não apenas estamos evitando reiventar a roda, como estamos utilizando um roda muito bem testada e amplamente usada. O Rspec é hoje, a ferramenta número 1 para muitos desenvolvedores Ruby, e mesmo a gente não precisando de muitos recursos que o Rspec pode fornecer, como por exemplo mocks, ele atende muito bem a nossas necessidades, que são básicas. Além disso, deixa os testes muito mais claros, e assim fica mais fácil para você ou uma outra pessoa dá manutenção mais pra frente.

Lógico que ele não é a única boa opção, temos por exemplo, o TestUnit que é muito bom por sinal, e se a gente quiser deixar mais alto nível ainda os nossos testes, podemos usar o Cucumber.

A mensagem que quero passar, é que poderíamos muito bem ter continuado a escrever os nossos testes daquela forma antiga, porém há formas melhores de automatizar os nossos testes. E isso não é sobre fazer “bunitinho” pra publicar num post, e sim a respeito de fazer “bunitinho” para tornar o seu trabalho mais fácil a médio e longo prazo.

O projeto

O projeto do post está disponível no meu Github (a versão 1.1 é a desse post – o link já vai pra ela). Para você utilizar ele, basta fazer clone do projeto (git clone git@github.com:FabricioFFC/watir_tests.git – necessário ter o git instalado).

Com o projeto em mãos, instale o Bundler (gem install bundler) e depois rode o comando bundle install, para instalar o Watir e o Rspec. Não se esqueça, que caso você queira testar usando o Internet Explorer, será necessário alterar dois arquivos: Gemfilehome_page.rb. Em ambos, comente a linha require firewatir e descomente a linha require watir.

Se tiver alguma dúvida ou problema para rodar os testes, só avisar nos comentários. E sugestões, críticas e opiniões sobre o projeto são bem-vindas também.

Até a próxima.

Fique por dentro das novidades, assine o feed do QualidadeBR.

OBS.: Ainda não testei no Windows, só no Ubuntu. Mas não deve ter diferenças. Se alguém encontrar algum problema, avise por favor.

[1] http://blog.davidchelimsky.net/2007/05/14/an-introduction-to-rspec-part-i/

Testando o Twitter com o Watir

No último post, vimos como instalar o Watir e escrever um teste usando ele.

Neste iremos nos aprofundar mais no Watir e para isso nada melhor do que testar.

Após a leitura do post, você estará sabendo:

  • Como modelar os seus testes utilizando orientação a objetos;
  • A importância de utilizar orientação a objetos nos seus testes;
  • Novos métodos da API do Watir;
  • Como fazer asserções de eventos assíncronos (AJAX).

Contexto

A equipe do Twitter ficou sabendo que existe uma comunidade brasileira que manja muito de Teste de Software.  Então, decidiram terceirizar para nós, a automação de alguns testes do Twitter.

Embora a parte da equipe do Twitter ter feito essa solicitação não seja verdade (hehe), somos pró-ativos, não somos?

Nossa missão é testar a funcionalidade de enviar uma mensagem no Twitter, o bom e velho “tweetar”. Os cenários de teste que temos são os seguintes:

  • Enviar uma mensagem válida;
  • Enviar uma mensagem repetida;
  • Enviar uma mensagem com mais de 140 caracteres;
  • Enviar uma mensagem em branco.

Típicos testes de sistema, então vamos colocar a mão na massa!

Como iremos modelar os testes?

Lembra do nosso primeiro teste usando o Watir? Ele é simplesmente um script de teste. Podemos muito bem fazer todos os nossos testes daquela maneira. Porém, vamos usar um pouco a nossa imaginação, para entender como o nosso mundo pode estar, se seguimos essa abordagem.

Temos mais de 100 testes, todos espalhados em scripts de teste, alguns contém só algumas linhas, outros são quase uma dissertação. Todos eles são gerenciados por um script mestre, que chama os scripts de teste e vai armazenando os resultados, para depois “printar” na tela.

Eu chamo o cenário acima de CAOS.

Automatizar os testes dessa maneira, é criar uma bola de neve que só irá aumentar, aumentar e um dia ninguém mais irá consegui movimentar ela. E aí vão culpar a ferramenta que é um lixo, ou a aplicação sob teste, quando na verdade o culpado é a própria equipe, que deu maior importância na criação dos testes, mas que acabou se esquecendo de organizar/estruturar a criação deles.

Além disso, o cenário acima mostra claramente uma reivenção da roda sem nenhuma necessidade: o gerenciamento dos testes. Hoje há muitas ferramentas que podem fazer isso para nós, no ecossistema Ruby, temos o TestUnit e o Rspec, por exemplo.

Uma abordagem mais efetiva, é utilizar orientação a objetos ao modelar os testes. Desta maneira será muito mais fácil dá manutenção no código e reutilizar-lo.

Como iremos aplicar OO nos nossos testes?

Como você sabe (se não sabe, deveria saber – leia esse livro e/ou essa apostila para aprender), a orientação a objetos é uma forma de organizar o nosso código, que nos orienta a trabalhar com objetos (não dá para ficar explicando conceitos de OO neste post, pois ele iria ficar enorme e minha explicação não seria melhor que a do Matt ou do pessoal da Caelum).

No nosso caso, uma página seria um objeto. Faz sentido não faz? Afinal, uma página contém atributos (os elementos HTML) e ações (as funcionalidades que ela provê).

Todos os nossos testes sehrão feitos numa mesma página, a página home do Twitter , portanto iremos instanciar o objeto HomePage da classe HomePage.

A primeira versão da nossa classe HomePage contém os seguintes métodos:

  • visit – acessa a home page do Twitter (twitter.com);
  • login – fará login no Twitter;
  • type_message – digita uma mensagem;
  • tweet – clica no botão para enviar uma mensagem;
  • message_exists? – verifica se uma mensagem existe na timeline;
  • alert_message_exists? – verifica se uma mensagem existe na página;
  • tweet_button_is_disable? – verifica se o botão para enviar a mensagem está desabilitado.

Falta alguma coisa, não falta não?

Os atributos da nossa classe.

Isso mesmo, mas vamos deixar eles de lado no momento, para não complicar demais o entendimento do nosso exemplo, afinal estamos começando, então vamos com calma e deixar eles para uma refatoração em um futuro post.

Você irá notar que, ao invés, de interagir com os atributos da classe HomePage, os nossos métodos estão usando o objeto browser para lidar com os elementos da página. Ou seja, abstrair os atributos da home page do Twitter, é luxo para a nossa primeira versão.

Testando o Twitter

Vamos parar com o blá-blá e ir para o código. Primeiro a classe que iremos utilizar nos nossos testes:

require 'rubygems'
require 'watir'
#require 'firewatir' => se você estiver no Linux/Mac

class HomePage

  HOME_PAGE = 'twitter.com'

  def initialize
    @browser = Watir::Browser.new
  end

  def visit
    @browser.goto(HOME_PAGE)
  end

  def login(username, password)
    @browser.text_field(:name, 'session[username_or_email]').value = username
    @browser.text_field(:name, 'session[password]').value = password
    @browser.button(:class, 'submit button').click
  end

  def type_message(message)
    @browser.text_field(:class, 'twitter-anywhere-tweet-box-editor').value = message
    @browser.text_field(:class, 'twitter-anywhere-tweet-box-editor').fire_event("onMouseDown")
  end

  def tweet
    @browser.link(:class, "tweet-button button").click
  end

  def message_exists?(message)
    @browser.wait_until {@browser.div(:class, 'tweet-text').text == message}
  end

  def alert_message_exists?(message)
    @browser.wait_until {@browser.text.include? message}
  end

  def tweet_button_is_disabled?
    @browser.link(:class, "tweet-button button disabled").exists?
  end

end

Aí está a nossa classe bonitona. Nela conseguimos abstrai várias ações que podemos fazer na página home do Twitter, desde preenchimentos até verificações de elementos.

Antes de irmos para os nossos testes propriamente ditos, vamos dá uma olhada nos métodos da API do Watir que estamos usando pela primeira vez:

  • @browser.text_field(:class,  ‘twitter-anywhere-tweet-box-editor’).value – no nosso primeiro teste usamos o set para preencher o text_field, agora estamos usando o value, simplesmente porque o set tem um bug no Firewatir (se você está testando usando o IE, e assim usando o Watir e não o Firewatir, não terá esse problema), e uma das formas de contornar ele, é usar o value;
  • @browser.text_field(:class,  ‘twitter-anywhere-tweet-box-editor’).fire_event(“onMouseDown”) –  o fire_event possibilita disparar um evento num elemento. No caso nosso seria como se a pessoa estivesse com o mouse em cima do text_field para digitar a mensagem e clica nele. Precisamos fazer isso, pois só assim o botão (que na verdade é um link) para “tweetar” será habilitado, uma vez que ele depende do disparo desse evento Javascript;
  • @browser.wait_until – o Watir é bem espertinho quanto a espera para carregar a página, esperando realmente ela carregar, e assim não precisamos ficar doidos adicionando sleeps. Porém ele não é mágico e com o wait_until podemos esperar a nossa mensagem aparecer na timeline, tendo ele um timeout de 60 segundos, o que é muito bom, pois não queremos esperar infinitamente. Estamos usando ele, pois a nossa mensagem aparece na timeline de forma assíncrona (AJAX), ou seja, a página não é carregada novamente, e então não podemos utilizar um @browser.text.included?(‘nossa mensagem’);
  • @browser.link(:class, “tweet-button button disabled”).exists? – este método é auto-explicativo, ele verifica se o elemento existe ou não na página. Estamos usando ele, pois a forma de descobrir se o link para “tweetar” a mensagem está habilitado ou não, é através da classe css do link, que quando desabilitado também terá a classe disabled.

Feitas as devidas explicações, vamos vê como ficam os nossos testes utilizando a classe HomePage:

USERNAME = 'SEU USUÁRIO NO TWITTER'
PASSWORD = 'A SUA SENHA NO TWITTER'
MESSAGE = 'testando o Twitter'

MESSAGE_WITH_MORE_THAN_140_CARACTERS = 'testando o limite de caracteres na mensagem no  Twitter, chegou? ainda nao? nao? nao? ainda nao? quando vamos chegar? estamos perto? chegamos!'

TWITTER_REPEATED_MESSAGE = 'Whoops! You already said that...'

#Teste 1 - Enviar uma mensagem válida

home_page = HomePage.new
home_page.visit
home_page.login(USERNAME,PASSWORD)
home_page.type_message(MESSAGE)
home_page.tweet

message = 'O teste enviar uma mensagem válida'
if home_page.message_exists? MESSAGE
 puts "#{message} PASSOU"
else
 puts "#{message} FALHOU"
end

#Teste 2 - Enviar uma mensagem repetida

home_page.type_message(MESSAGE)
home_page.tweet

message = 'O teste enviar uma mensagem repetida'
if home_page.alert_message_exists? TWITTER_REPEATED_MESSAGE
 puts "#{message} PASSOU"
else
 puts "#{message} FALHOU"
end

#Teste 3 - Enviar uma mensagem com mais de 140 caracteres

home_page.type_message(MESSAGE_WITH_MORE_THAN_140_CARACTERS)

message = 'O teste enviar uma mensagem com mais de 140 caracteres'
if home_page.tweet_button_is_disabled?
 puts "#{message} PASSOU"
else
 puts "#{message} FALHOU"
end

#Teste 4 - Enviar uma mensagem em branco

home_page.type_message('')

message = 'O teste enviar uma mensagem em branco'
if home_page.tweet_button_is_disabled?
 puts "#{message} PASSOU"
else
 puts "#{message} FALHOU"
end

Com certeza os nossos testes ficaram bem melhor utilizando a classe HomePage, em comparação se a gente fosse fazer sem ela. No entanto, eles estão BEM RUINS, em comparação com os testes do próximo post. 😀

A intenção dos posts não é dá “a melhor” solução de bandeja, afinal o melhor da jornada da automação dos testes (e de qualquer outra jornada), é justamente o caminho, se eu mostrasse logo no primeiro post a solução final, a gente não ia entender os motivos pelos quais ela é uma solução melhor que as anteriores. Talvez até seria possível entender, mas evoluindo aos poucos, irá ficar muito mais clara a diferença desses testes porcos, para os da solução final.

De qualquer modo, já atendemos a demanda que foi solicitada pelo pessoal do Twitter (hehe). O que iremos vê no próximo post, será como melhorar o código dos nossos testes, utilizando o Rspec. Até lá!

Observações

  • Disponibilizei no meu Gist, o código completo do post: http://bit.ly/ges0xH
  • O código foi testado no Windows com o IE8 e também no Ubuntu usando o Firefox 3.6 (lembrando que para testar usando o Firefox, você tem que usar o firewatir e não o watir).

Saiba mais

Para saber mais sobre o Watir, acesse o meu delicious, lá estou guardando vários links a respeito do Watir, e não deixe de visitar a página oficial do projeto, lá tem vários bons exemplos e explicações.

Fique por dentro das novidades, assine o feed do QualidadeBR.

Conheça o Watir

O objetivo deste post é fazer uma introdução a ferramenta Watir (mesma pronúncia de water). Como sendo uma introdução, irei abordar o básico do básico, até porque a minha experiência com o Watir são de menos de 1 hora.

Ao terminar a leitura deste post, você estará sabendo:

  • O que é o Watir
  • Porquê usar o Watir
  • Como instalar o Watir
  • Realizar um teste básico com o Watir

O que é o Watir?

Primeiro a resposta curta de quem não conhece muito: Um Selenium desenvolvido em Ruby, cujo seus testes também serão escritos em Ruby.

Agora uma resposta apoiada no conteúdo da página oficial do Watir: O Watir é uma ferramenta para você testar aplicações Web, sob a licença BSD. Ela é um conjunto de bibliotecas em Ruby que permite você automatizar os seus testes. Você escreve os seus testes em Ruby e pode rodá-los no IE, Firefox, Chrome, Safari e Opera.

Ou seja, a minha primeira resposta é válida, porém está longe de ser a mais completa. No entanto, de início vamos com ela, afinal só iremos ter uma melhor resposta usando o Watir. 🙂

Por que usar o Watir?

Os motivos para usar o Watir são [1]:

  • É uma ferramenta open source e não há custos para usar;
  • A comunidade do Watir está crescendo e é muita ativa;
  • Ele usa Ruby, uma linguagem de script moderna e completa e que possui uma baixa curva de aprendizado (em comparação com Java, por exemplo);
  • Ele suporta qualquer aplicação web, não importando a linguagem na qual ela foi desenvolvida;
  • O Watir suporta múltiplos browsers e diferentes plataformas;
  • Ele é poderoso, fácil de usar e ainda é bem leve.

Como instalar o Watir

Para instalar o Watir o único pré-requisito é ter instalado o Ruby (versão 1.8.6 ou 1.8.7). Portanto vamos instalar, lembrando que os próximos passos foram feitos no Windows (se você usa Linux, vá no tópico Instalando no Linux):

  1. Instale o Ruby 1.8.7-p334 (saiba mais em RubyInstaller);
  2. Ao iniciar a instalação, logo após aceitar os termos, marque os dois checkboxes (“Add Ruby executables to your PATH” e “Associate .rb and .rbw files with this Ruby installation”) e clique em Install;
  3. Ao terminar a instalação, vá no prompt de comando (ctrl+r e depois digite cmd) e digite os seguintes comandos:
    • ruby -v (deverá retornar a versão do Ruby que foi instalada, ex: ruby 1.8.7)
    • gem -v (deverá retornar a versão do RubyGems que foi instalada, ex: 1.5.2)
    • irb (deverá entrar no irb – o shell interativo do Ruby), dentro dele escreva:
      • puts “hello world”
    • depois digite exit para sair do irb.

Se tudo deu certo, parabéns o Ruby foi corretamente instalado e de quebra você escreveu um Hello World em Ruby. 😉

Agora vamos instalar o Watir:

  1. No prompt do Windows digite: gem install watir

Pronto só isso, se nenhum erro ocorreu, já temos o Watir instalado. Caso algum erro tenha ocorrido, consulte o último tópico Erros comuns.

Realizando um teste com o Watir

Segue abaixo, um screencast mostrando como realizar um teste simples com o Watir (assistir em 720p é recomendado). Estarei fazendo o teste usando o irb:

O teste executado foi baseado no exemplo do site oficial do Watir, para vê outros exemplos acesse essa página.

Você pode fazer o mesmo teste na sua máquina, de duas formas:

  • Pelo irb, como fiz no vídeo, informando cada “comando” no irb;
  • Ou de uma forma mais fácil, baixando esse arquivo, que contém todo o script de teste completo. Além dos código que foi informado no irb, tem também vários comandos e puts, para facilitar o entendimento do código. Lembrando que após baixar o arquivo, é só executar o watir_example.rb, que um prompt abrirá e nele serão “printados” os passos e também o resultado do teste.

Conclusão

Como pudemos ver no vídeo, o Watir interage diretamente com o browser. Nós só precisamos conhecer a API dele, para podermos criar os nossos próprios testes, e é claro também um pouco de Ruby irá ajudar.

Resumindo, o que precisamos saber para criar o nosso próprio teste usando o Watir é o seguinte:

  • O valor da propriedade name dos elementos que iremos ter que interagir para poder fazer os passos do nosso teste (o desenvolvedor pode passar essa informação para você, ou você pode encontrar usando o Firebug);
    • Numa rápida pesquisa, parece que existem IDEs de record-play (estilo o Selenium IDE) para o Watir, porém nenhuma é mantida oficialmente.
  • A API do Watir, basicamente conhecer quais são os métodos que podemos usar, quando usar e quais parâmetros passar;
  • A linguagem Ruby, mas calma não precisa aprender toda a linguagem de uma vez, vá com calma, comece primeiro com exemplos básicos (como esse que mostrei) e vá incrementando eles.

Bem é isso. Qualquer dúvida, crítica ou sugestão, sintam-se à vontade em deixar nos comentários.

Instalando no Linux

Para instalar o Ruby, Rubygems e Watir no Linux, preparei um script (testado no Ubuntu). Basta baixar ele, alterar a permisão (chmod +x watir_installation.sh) e rodar o script com sudo (sudo ./watir_installation.sh).

Uma coisa que muda no script do teste, é que ao invés, de fazer o require no watir, você irá fazer o require no firewatir (require firewatir), que é o driver para o Firefox. Além disso, você terá que instalar esse plugin no Firefox. Maiores detalhes, é só conferir o procedimento de instalação que está disponível no site do Watir (lá tem para quem usa Mac também).

Erros comuns

Erro: ERROR:  While executing gem … (Errno::ENXIO)

Solução:

  • Vá no diretório C:\Ruby187\bin (ou no local que você especificou para instalar o Ruby na sua instalação);
  • Faça uma cópia do arquivo gem (para fazer backup);
  • Edite esse mesmo arquivo (gem), alterando o seu conteúdo para o que está aqui.

Fique por dentro das novidades, assine o feed do QualidadeBR.

Fonte:

[1] http://watir.com/