Gabriel Zuqueto Amaral
www.gabrielzuqueto.eti.br

Abstrair AWS SQS no Ruby Parte 2: Criando testes automatizados

Este post é a segunda parte do post Abstrair AWS SQS no Ruby. Nele mostro como criar testes automatizados para a classe que criamos (QueueManager)

Abstrair AWS SQS no Ruby Parte 2: Criando testes automatizados

No post anterior escrevemos uma classe chamada QueueManager, que é uma abstração do SDK da AWS para o SQS. Agora vamos ver como criar testes automatizados para a classe que criamos.

A primeira coisa que devemos fazer é alterar o arquivo spec_helper.rb para que consigamos criar os stubs para o AWS SQS. Além disso, devemos fazer o require tanto do aws-sdk quanto do queue_manager.rb para que os nossos testes automatizados funcionem.

Lembrando que estamos utilizando o Ruby 2.2.7 e aws-sdk 2.9.11.

Preparando o ambiente dos testes automatizados

Dentro do diretório do nosso projeto, abra ./spec/spec_helper.rb no seu editor de texto preferido e deixe o conteúdo da seguinte forma:

require 'aws-sdk'
require 'queue_manager.rb'

RSpec.configure do |config|
  config.expect_with :rspec do |expectations|
    expectations.include_chain_clauses_in_custom_matcher_descriptions = true
    expectations.syntax = [:should, :expect]
  end

  config.mock_with :rspec do |mocks|
    mocks.verify_partial_doubles = true
  end

  config.shared_context_metadata_behavior = :apply_to_host_groups

  Aws.config.update(stub_responses: true)
end

Como foi dito logo no segundo parágrafo deste post, fizemos os requires necessários, após temos algumas configurações básicas do rspec, e na penúltima linha habilitamos o stub para o AWS SDK v2.

Feito isso, vamos para o próximo passo.

Criando o esqueleto do arquivo dos testes automatizados

Vamos começar criando o arquivo que conterá os testes. Para isso, dentro do diretório do projeto crie um arquivo com o nome de queue_manager_spec.rb, dentro do diretório spec:

touch spec/queue_manager_spec.rb

Abra o arquivo e ponha os seguintes comandos dentro dele e depois salve:

require 'spec_helper.rb'
describe "QueueManager" do
  let(:queue_url){"https://sqs.us-west-2.amazonaws.com/943154236803/gabrielzuqueto_eti_br"}
  let(:queue_name){:gabrielzuqueto_eti_br}
  let(:client_aws_sqs){Aws::SQS::Client.new}

  before do
    allow_any_instance_of(QueueManager).to receive(:client).and_return client_aws_sqs
  end

end

Bem simples! Adicionamos o arquivo responsável pelas configurações globais dos nossos testes e depois iniciamos um bloco com a descrição do arquivo de teste. Na descrição botamos QueueManager para que fique claro que os testes sãopara esta classe.

Dentro criamos três métodos para nos auxiliar nos testes automatizados. O primeiro retorna a URL da fila, o segundo retorna o nome da fila e o terceiro retorna uma instância do client da AWS para SQS. Iremos utilizá-los nos stubs.

Por último dissemos para o teste que todas vez que o método client da classe QueueManager for chamado, ele irá retornar o client que iremos utilizar para configurar os stubs do SQS. Fizemos nosso primeiro stub!

Vamos aos testes!

Primeiro e segundo teste automatizado

Vamos testar a criação de uma instância da nossa classe e vamos verificar se os parâmetros que enviamos foram guardados nesta nova instância. Para isso faremos 2 testes, um com o valor default do parâmetro visibility_timeout e outro setando um novo valor para este argumento.

  describe "When try instance object" do
    it "should return a new instance of QueueManager visibility_timeout=60" do
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ queue_name } ).to eq(queue_name)
      expect( queue_manager.instance_eval{ visibility_timeout } ).to eq(60)
    end

    it "with visibility_timeout arg equals 120 should return a new instance of QueueManager visibility_timeout=120" do
      queue_manager = QueueManager.new(queue_name, 120)
      expect( queue_manager.instance_eval{ queue_name } ).to eq(queue_name)
      expect( queue_manager.instance_eval{ visibility_timeout } ).to eq(120)
    end
  end

Ponha este código antes do último end do arquivo, salve e vamos executar o teste para ver se está tudo Ok.

bundle exec rspec

Para a nossa surpresa, o primeiro teste quebrou!

F.

Failures:

  1) QueueManager When try instance object should return a new instance of QueueManager visibility_timeout=60
     Failure/Error:
       def initialize(queue_name, visibility_timeout = 1.minutes)
         @queue_name = queue_name
         @visibility_timeout = visibility_timeout
       end
     
     NoMethodError:
       undefined method `minutes' for 1:Fixnum
     # ./lib/queue_manager.rb:2:in `initialize'
     # ./spec/queue_manager_spec.rb:13:in `new'
     # ./spec/queue_manager_spec.rb:13:in `block (3 levels) in <top (required)>'

Finished in 0.14077 seconds (files took 0.14085 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/queue_manager_spec.rb:12 # QueueManager When try instance object should return a new instance of QueueManager visibility_timeout=60

Isso aconteceu pois utilizamos o método minutes no valor default do visibility_timeout, no método initialize da class QueueManager. O problema é que o Rails é quem provê este método e nós estamos Ruby puro.

Reparou o quanto é importante ter testes automatizados? Se não fosse por ele, este código provavelmente iria para produção e quebraria a aplicação.

Vamos corrigir a classe QueueManager!

Abra lib/queue_manager.rb em seu editor de texto e na linha 2 altere visibility_timeout = 1.minutes por visibility_timeout = 60.

class QueueManager
  def initialize(queue_name, visibility_timeout = 60)
    @queue_name = queue_name
    @visibility_timeout = visibility_timeout
  end
...

Pronto! Salve o arquivo e execute os testes novamente.

..

Finished in 0.15856 seconds (files took 0.1605 seconds to load)
2 examples, 0 failures

Corrigimos o bug! :D

Terceiro teste automatizado

Vamos testar o método client da classe QueueManager. Este método deve retornar uma instância da classe Aws::SQS::Client.

  describe "When call client method" do
    it "should return a instance of Aws::SQS::Client" do
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ client } ).to be_instance_of(Aws::SQS::Client)
    end
  end

Adicione estas linhas antes do último end do nosso arquivo de teste, salve o arquivo e execute os testes.

...

Finished in 0.13722 seconds (files took 0.14355 seconds to load)
3 examples, 0 failures

Tudo certo, vamos para o quarto teste.

Quarto teste automatizado

Vamos agora criar o teste para o método privador create_queue da classe QueueManager. Adicione estas linhas antes do último end do nosso arquivo de testes automatizados:

  describe "When call create_queue method" do
    it "should return the queue url" do
      client_aws_sqs.stub_responses(:create_queue, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ create_queue } ).to eq(queue_url)
    end
  end

Básicamente estamos criando um stub para o método create_queue da classe Aws::SQS::Client. Logo, se tudo estiver certo, ao chamar o create_queue da classe QueueManager, será retornada a URL da fila como esperamos. Salve o arquivo e execute os testes.

Mais uma vez, quebrou! O motivo é o mesmo: Utilizamos métodos providos pelo Rails em um projeto de Ruby puro.

...F

Failures:

  1) QueueManager When create queue should return the queue url
     Failure/Error: client.create_queue({queue_name: queue_name},{:http_open_timeout => 1.hours, :http_read_timeout => 1.hours, :logger => nil, :visibility_timeout => 1.minutes}).queue_url
     
     NoMethodError:
       undefined method `hours' for 1:Fixnum
     # ./lib/queue_manager.rb:90:in `create_queue'
     # ./spec/queue_manager_spec.rb:36:in `block (4 levels) in <top (required)>'
     # ./spec/queue_manager_spec.rb:36:in `instance_eval'
     # ./spec/queue_manager_spec.rb:36:in `block (3 levels) in <top (required)>'

Finished in 0.18209 seconds (files took 0.16784 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/queue_manager_spec.rb:33 # QueueManager When create queue should return the queue url

Vamos corrigir!

Abra lib/queue_manager.rb em seu editor de texto e na linha 90 altere:

client.create_queue({queue_name: queue_name},{:http_open_timeout => 1.hours, :http_read_timeout => 1.hours, :logger => nil, :visibility_timeout => 1.minutes}).queue_url

Por:

client.create_queue({queue_name: queue_name},{:http_open_timeout => 3600, :http_read_timeout => 3600, :logger => nil, :visibility_timeout => 60}).queue_url

Salve o arquivo e rode os testes novamente.

....

Finished in 0.22548 seconds (files took 0.14047 seconds to load)
4 examples, 0 failures

Mais um bug corrigido! Provavelmente teremos mais.

Quinto e sexto teste automatizado

Agora vamos criar os testes para o método get_queue_url da classe QueueManager. Precisamos de 2 testes, pois ele pergunta ao SQS a URL de fila e caso amesma não exista, ele a cria. logo, são 2 comportamentos que devemos testar.

  describe "When call get_queue_url method" do
    it "should return the queue url" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ get_queue_url } ).to eq(queue_url)
    end

    it "if queue not exists should create queue and return your URL" do
      client_aws_sqs.stub_responses(:get_queue_url, 'NonExistentQueue')
      client_aws_sqs.stub_responses(:create_queue, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ get_queue_url } ).to eq(queue_url)
    end
  end

No primeiro teste é como se a fila já existisse, logo ele retorna a URL de cara.

No segundo a gente criar o stub de erro de fila não existente para o método get_queue_url do Aws::SQS::Client e após a gente cria o stub de fila criada para o método create_queue da mesma classe dita anteriormente. Após a gente testa se a URL da fila foi retornada, assim garantindo que o método get_queue_url da classe QueueManager está funcionando!

Execute novamente os testes.

......

Finished in 0.23801 seconds (files took 0.14207 seconds to load)
6 examples, 0 failures

Tudo correto, vamos para o próximo!

Sétimo teste automatizado

Agora iremos testar o método queue_url da classe QueueManager.

  describe "When call queue_url method" do
    it "should return the queue url" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.instance_eval{ queue_url } ).to eq(queue_url)
    end
  end

Mais uma vez adicione estas linhas antes do último end do nosso arquivo de teste, salve e execute os testes.

.......

Finished in 0.24331 seconds (files took 0.13894 seconds to load)
7 examples, 0 failures

Tudo certo! Próximo!

Agora que todos os métodos privados foram testados e o método público initialize também, iremos começar a testar os outros métodos públicos. Sim, todos os métodos privados foram testados, pois visibility_timeout e queue_name da classe QueueManager foram testados logo no primeiro teste.

Oitavo teste automatizado

Vamos testar o método send_message da classe QueueManager. Vamos verificar apenas se não houve erro ao chamar o método.

  describe "When call send_message method" do
    it "should not return error" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.send_message("Something") }.to_not raise_error
    end
  end

Adicione estas linhas no arquivo de teste como fez com os outros, salve e execute os testes.

.......F

Failures:

  1) QueueManager When call send_message method should not return error
     Failure/Error: expect { queue_manager.send_message("Something") }.to_not raise_error
     
       expected no Exception, got #<NameError: undefined local variable or method `data' for #<QueueManager:0x007f7d0c8bac00>> with backtrace:
         # ./lib/queue_manager.rb:8:in `send_message'
         # ./spec/queue_manager_spec.rb:67:in `block (4 levels) in <top (required)>'
         # ./spec/queue_manager_spec.rb:67:in `block (3 levels) in <top (required)>'
     # ./spec/queue_manager_spec.rb:67:in `block (3 levels) in <top (required)>'

Finished in 0.26467 seconds (files took 0.14186 seconds to load)
8 examples, 1 failure

Failed examples:

rspec ./spec/queue_manager_spec.rb:64 # QueueManager When call send_message method should not return error

Mais um bug! Em lib/queue_manager.rb:8, estamos tentando enviar o conteúdo da variável data, porém esta variável não existe. Altere para message_data, salve o arquivo.

class QueueManager
  def initialize(queue_name, visibility_timeout = 60)
    @queue_name = queue_name
    @visibility_timeout = visibility_timeout
  end

  def send_message(message_data)
    client.send_message({queue_url: queue_url, message_body: message_data })
  end
...

Execute os testes e vamos em frente!

........

Finished in 0.24677 seconds (files took 0.14044 seconds to load)
8 examples, 0 failures

Nono e décimo teste automatizado

Vamos criar dois testes para o método receive_message da classe QueueManager: um para o caso de não existir mensagens na fila e o outro caso exista.

  describe "When call receive_message method" do
    before do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
    end

    it "and there is no message should return response with messages array empty" do
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.receive_message.messages.count ).to eq(0)
    end

    it "should return not empty message array" do
      client_aws_sqs.stub_responses(:receive_message, messages: [
        message_id: "Something",
        receipt_handle: "Something",
        body: {Something: "Something"}.to_json
      ])
      expected_message = [Aws::SQS::Types::Message.new(message_id: "Something", receipt_handle: "Something", body: {"Something"=>"Something"}.to_json, attributes: {}, message_attributes: {})]
      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.receive_message.messages).to eq(expected_message)
    end
  end

Adicione ao seu arquivo de teste, salve e execute os testes automatizados.

..........

Finished in 0.26132 seconds (files took 0.14142 seconds to load)
10 examples, 0 failures

Décimo primeiro teste automatizado

Vamos testar o método delete_message da classe QueueManager. Como no método send_message, iremos apenas testar se não ocorreu erro ao chamar o método do delete_message da classe Aws::SQS::Client.

  describe "When call delete_message method" do
    it "should not return error" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.delete_message("Something") }.to_not raise_error
    end
  end

Já sabemos o que fazer a cada teste novo…

...........

Finished in 0.26799 seconds (files took 0.1416 seconds to load)
11 examples, 0 failures

Tudo Ok! Próximo! :D

Décimo segundo teste automatizado

Vamos agora testar o método send_message_batch da classe QueueManager. O teste será parecido com os outros 2 anteriores.

  describe "When call send_message_batch method" do
    it "should not return error" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.send_message_batch([{id: "1", message_body: "Something"},{id: "2", message_body: "Anything"}]) }.to_not raise_error
    end
  end

Note que é obrigatório enviar um ID no JSON da mensagem. Veja mais detalhes na documentação oficial do SDK AWS v2.

Executando todos os testes temos:

............

Finished in 0.50623 seconds (files took 0.21058 seconds to load)
12 examples, 0 failures

Perfeito!

Décimo terceiro teste automatizado

Agora vamos criar o teste do método receive_message_batch da classe QueueManager.

  describe "When call receive_message_batch method" do
    it "should return messages array" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      client_aws_sqs.stub_responses(:receive_message, messages: [
        {message_id: "Something",
        receipt_handle: "Something",
        body: {Something: "Something"}.to_json},
        {message_id: "Something_2",
        receipt_handle: "Something_2",
        body: {Something: "Something_2"}.to_json}
      ])

      expected_message = [
        Aws::SQS::Types::Message.new(message_id: "Something", receipt_handle: "Something", body: {"Something"=>"Something"}.to_json, attributes: {}, message_attributes: {}),
        Aws::SQS::Types::Message.new(message_id: "Something_2", receipt_handle: "Something_2", body: {"Something"=>"Something_2"}.to_json, attributes: {}, message_attributes: {})
      ]

      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.receive_message_batch.messages).to eq(expected_message)
    end
  end

Ao executar os testes, teremos o seguinte resultado:

.............

Finished in 0.56844 seconds (files took 0.20844 seconds to load)
13 examples, 0 failures

Décimo quarto teste automatizado

Agora vamos testar a exclusão por batch. Para isso, vamos criar um teste para o método delete_message_batch da classe QueueManager.

  describe "When call delete_message_batch method" do
    it "should return messages array" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.delete_message_batch([{id: "Something", receipt_handle: "Something"}]) }.to_not raise_error
    end
  end

Mais uma vez, vamos rodar novamente os teste.

..............

Finished in 0.51683 seconds (files took 0.20637 seconds to load)
14 examples, 0 failures

Décimo quinto teste automatizado

Vamos criar o teste para o método purge_queue da classe QueueManager.

  describe "When call purge_queue method" do
    it "should return messages array" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.purge_queue }.to_not raise_error
    end
  end

Ao executar novamente o arquivo de testes automatizados, temos:

...............

Finished in 0.51229 seconds (files took 0.23809 seconds to load)
15 examples, 0 failures

Décimo sexto teste automatizado

Estamos quase terminando a criação dos testes automatizados. Vamos criar o teste para o método delete_queue da classe QueueManager.

  describe "When call delete_queue method" do
    it "should return messages array" do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      queue_manager = QueueManager.new(queue_name)
      expect { queue_manager.delete_queue }.to_not raise_error
      expect( queue_manager.instance_eval{ @queue_url } ).to eq(nil)
    end
  end

Neste teste verificamos também a variável @queue_url, pois sabemos que ao excluir a fila, a gente seta esta variável com nulo.

................

Finished in 0.54293 seconds (files took 0.19049 seconds to load)
16 examples, 0 failures

Décimo sétimo, Décimo oitavo, Décimo novo e Vigésimo teste automatizado

Vamos criar os testes que retornam a quantidade de mensagens na fila.

  describe "When call " do
    before do
      client_aws_sqs.stub_responses(:get_queue_url, queue_url: queue_url)
      client_aws_sqs.stub_responses(:get_queue_attributes, attributes: {
        "ApproximateNumberOfMessages" => "1",
        "ApproximateNumberOfMessagesNotVisible" => "1",
        "ApproximateNumberOfMessagesDelayed" => "1"
      })
    end

    it "queue_size method should return queue size" do
      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.queue_size).to eq(3)
    end

    it "queue_available_size method should return queue available size" do
      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.queue_available_size).to eq(1)
    end

    it "queue_unavailable_size method should return queue unavailable size" do
      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.queue_unavailable_size).to eq(1)
    end

    it "queue_waiting_size method should return queue waiting size" do
      queue_manager = QueueManager.new(queue_name)
      expect(queue_manager.queue_waiting_size).to eq(1)
    end
  end

A diferença neste teste é que criamos o stub para o método get_queue_attributes da classe Aws::SQS::Client.

Vamos executar os testes :)

....................

Finished in 0.58918 seconds (files took 0.20795 seconds to load)
20 examples, 0 failures

Tudo Ok. Agora vamos para o último teste da nossa classe.

Vigésimo primeiro e último teste automatizado

Este teste é bem simples. Verificamos apenas se o método poller da classe QueueManager retorna uma instância da classe Aws::SQS::QueuePoller.

  describe "When call poller method" do
    it "should return a instance of Aws::SQS::QueuePoller" do
      queue_manager = QueueManager.new(queue_name)
      expect( queue_manager.poller ).to be_instance_of(Aws::SQS::QueuePoller)
    end
  end

Rodando o teste, temos:

.....................

Finished in 0.53894 seconds (files took 0.2233 seconds to load)
21 examples, 0 failures

 

Chegamos ao final deste post! Conseguimos cobrir 100% da nossa classe QueueManager por testes automatizados.

Pudemos ver na prática, como testes automatizados são importantes em um projeto. Encontramos bugs apenas rodando os testes.

Baixe o código-fonte: https://github.com/gabrielzuqueto/queue_manager_aws_sdk_v2_sqs_ruby

Confira aqui mais detalhes sobre como utilizar o Aws::SQS::QueuePoller

https://gabrielzuqueto.eti.br/aws-sqs-ruby-testes-automatizados 2017-06-10 21:53:10 -0300 gabrielzuqueto

Deixe seu comentário

Não perca mais nenhum post!

Cadastre-se e receba novos posts diretamente em seu e-mail.

Escolhidos para você

RSpec Stub AWS SQS
RSpec Stub AWS SQS

Abstrair AWS SQS no Ruby
Abstrair AWS SQS no Ruby

Como usar GitHub?
Como usar GitHub?