tratamento de exceção e testes automatizados [práticas de desenvolvimento de software]
TRANSCRIPT
Práticas de Desenvolvimento de Software
Aula 7. 06/04/2015.
Oferecimento
Aula 7 - Tratamento de exceção; Testes automatizados
Parte 1 Tratamento de exceção
Definição
Tratamento de exceção é um mecanismo existente em algumas linguagens de programação para sinalizar e tratar a ocorrência de falhas que
acontecem durante a execução de um programa.
Tratamento de exceção
Observação: tratamento de exceção também existe em hardware, mas o foco desta aula é o uso deste mecanismo em software.
Tipos de falhas (1)
Tipos de falhasErros no uso de uma biblioteca
# HTTP client API for Ruby
require 'net/http'
# Try to download Google's home page
Net::HTTP.get('http://www.google.com')
# NoMethodError: undefined method `hostname' for "http://www.google.com":String
# from /Users/deborasetton/.rvm/ruby/2.2.0/net/http.rb:478:in `get_response'
# Try again, with URI object instead of String
uri = URI('http://www.google.com')
Net::HTTP.get(uri) # => String
Exemplos adaptados de: http://avdi.org/talks/exceptional-ruby-2011-02-04/
Tipos de falhas (2)
Situações não-previstas
if (value.is_a?(String)) # We can deal with a stringelsif (value.is_a?(Array)) # We can deal with an array tooelse # Uh oh... I don't know what to do here.end
print 'Type a number N from 1 to 10: 'number = gets.chomp.to_i # User types: "I don't want to"puts "(100 % N) is: #{100 % number}"# ZeroDivisionError: divided by 0# from (pry):9:in `__pry__'
Tipos de falhas
Exemplos adaptados de: http://avdi.org/talks/exceptional-ruby-2011-02-04/
Tipos de falhas (3)
Erros de programação
arr = [1, 2, 3]
puts "The last element incremented by 1 is: #{arr[3] + 1}"
# NoMethodError: undefined method `+' for nil:NilClass
# from (pry):2:in `__pry__'
Falhas em recursos externos
Net::HTTP.get('http://www.google.com')
# Net::ReadTimeout: Net::ReadTimeout
# from /Users/deborasetton/.rvm/ruby/2.2.0/net/protocol.rb:158:in `rescue in rbuf_fill'
Tipos de falhas
Exemplos adaptados de: http://avdi.org/talks/exceptional-ruby-2011-02-04/
Ocorrência de falha durante a execução
ProgramaBibilioteca 1
Bibilioteca 1
ruby app.rb
$ ruby app.rb /Users/deborasetton/Dev/github/infosimples/praticas-devsoft-2015-01/week7/code/lib2.rb:14:in `get_value': undefined method `+' for nil:NilClass (NoMethodError) from /Users/deborasetton/Dev/github/infosimples/praticas-devsoft-2015-01/week7/code/lib1.rb:10:in `convert' from /Users/deborasetton/Dev/github/infosimples/praticas-devsoft-2015-01/week7/code/example.rb:6:in `start' from /Users/deborasetton/Dev/github/infosimples/praticas-devsoft-2015-01/week7/code/example.rb:2:in `run' from /Users/deborasetton/Dev/github/infosimples/praticas-devsoft-2015-01/week7/code/example.rb:17:in `<main>'
… get_value()
start()
convert()
Resultado da execução:
Exceção (1)
If code in one routine encounters an unexpected condition that it doesn't know how to handle, it throws an exception,
essentially throwing up its hands and yelling "I don't know what to do about this–I sure hope somebody else knows how
to handle it!
Exceção
Steve McConnel — Code Complete
Sinaliza que uma falha ocorreu e interrompe o fluxo normal de execução
I don't know what to do about this–I sure hope somebody else
knows!
Exceção (2)
• A execução pode ser continuada apenas se alguma rotina da pilha de execução (call stack) conseguir lidar com essa falha, ou "tratar a exceção". Caso contrário, o programa termina.
• O termo "exceção" pode se referir tanto ao evento excepcional que aconteceu quanto à estrutura de dados que contém informações sobre esse evento. As informações básicas normalmente acessíveis são:
• Tipo do erro • Mensagem de erro • Stack trace (mostra o caminho percorrido pelo programa até chegar
ao ponto em que a exceção foi "lançada")
Sinaliza que uma falha ocorreu e interrompe o fluxo normal de execução
Exceção
Tratando uma exceção — Sintaxe em Ruby (1)
1 begin
2 # Do something
4 rescue SomeError
5 # This is executed when a SomeError exception
6 # is raised
7 rescue AnotherError => error
8 # Here, the exception object is referenced from the
9 # `error' variable
10 rescue
11 # This catches all exceptions derived from StandardError
12 retry # This executes the begin section again
13 else
14 # This is executed only if no exceptions were raised
15 ensure
16 # This is always executed, exception or not
17 end
Tratando uma exceção: sintaxe em Ruby
Adaptado de: http://en.wikipedia.org/wiki/Exception_handling_syntax
1 begin2 # O `begin` inicia um bloco de código que é considerado "arriscado",3 # isto é, sabe-se que algum ponto desse bloco poderá falhar e lançar uma4 # exceção. O código começa a ser executado normalmente e, se tudo correr bem,5 # continua até o final desse bloco.6 rescue SomeError7 # Esse bloco só será executado se acontecer uma exceção no bloco anterior e,8 # além disso, apenas se a exceção for do tipo (classe) SomeError.9 rescue AnotherError => error
10 # Esse bloco só será executado se a exceção for do tipo AnotherError.11 # Além disso, aqui nós temos acesso aos dados da exceção através do objeto12 # `error`.13 rescue14 # Esse bloco será executado se alguma exceção for lançada de um tipo que não15 # seja nem SomeError nem AnotherError.16 # Em outras palavras, esse bloco "trata" todas as exceções não capturadas em17 # blocos anteriores (e que sejam derivadas de StandardError).18 19 # Em um bloco "rescue", é possível usar "retry" para tentar novamente.20 retry21 else22 # Esse bloco só será executado em caso de sucesso23 ensure24 # Esse bloco será executado sempre (em caso de sucesso ou de erro) e por25 # último.26 end
Tratando uma exceção — Sintaxe em Ruby (2)
Tratando uma exceção: descrição
Exceções podem ser tratadas em qualquer ponto da call stack
ProgramaBibilioteca 1
Bibilioteca 1
ruby app.rb
… get_value()
start()
convert()
rescue ErrorX
rescue ErrorYrescue ErrorZ
• Quando uma exceção é lançada, o programa realiza uma busca na call stack, tentando encontrar um exception handler que declare saber tratar exceções daquele tipo.
• Isso significa que exceções podem ser tratadas em qualquer ponto da call stack, e não apenas pelo último método chamado.
• A ideia é que cada ponto do código fique responsável por tratar o erro da maneira como for melhor para o seu contexto, e só trate os erros que fizer sentido tratar.
Lançando uma exceção — Sintaxe em Ruby
Lançando uma exceção: sintaxe em Ruby
# Por padrão, a exceção é do tipo RuntimeError
raise 'An error just happened!'
# Você também pode dizer qual o tipo a ser usado (nesse caso, ArgumentError)
raise ArgumentError, 'Variable "arr" should be an Array, but is a Hash'
• O ponto de lançamento de uma exceção e o ponto de tratamento de uma exceção podem ser muito "distantes". Exemplo: uma biblioteca de tratamento de imagens usada pelo seu programa lança uma exceção que precisará ser tratada por você.
• O tipo da exceção deve ser escolhido de acordo com o significado da falha que aconteceu
• Os tipos de exceção normalmente têm uma hierarquia entre eles
Hierarquia de exceções em Ruby
Hierarquia de exceções em Ruby
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SecurityError
StandardError
ArgumentError
EncodingError
IOError
NameError
NoMethodError
RegexpError
RuntimeError
ThreadError
TypeError
ZeroDivisionError
SystemExit
fatal
Algumas classes na hierarquia de exceções de Ruby
Você pode criar outros tipos de erros específicos para sua biblioteca, API, etc — é só definir classes que herdam das classes ao lado.
# Definição de um novo tipo
class Geometry::NameError < NameError
end
# Mais tarde, em algum ponto do código...
raise Geometry::NameError, "Unknown polygon: Manysides-agon"
Adaptado de: http://blog.nicksieger.com/articles/2006/09/06/rubys-exception-hierarchy/
Mais sobre tratamento de exceção
• Tratar uma exceção deve se limitar ao mínimo de operações necessárias para
recuperar o estado do programa ou encerrá-lo de maneira adequada • Evite, dentro de um rescue, relançar outra exceção, de outro tipo, mudando o erro
original — isso pode mascarar a verdadeira causa do problema • Evite lançar exceções quando a situação de erro for previsível (exemplo: sempre
validar a entrada fornecida pelo usuário antes de passá-la para frente) • O abuso de exceções pode tornar o código mais difícil de compreender, porque os
caminhos possíveis para o programa não são óbvios • Alternativas existentes ao mecanismo de exceções: normalmente, códigos de erro • Diferentes maneiras de responder a uma falha: retornar um valor que indica erro
(normalmente nulo), retornar uma resposta "falsa" (fake) mas que permite a
continuação do programa, simplesmente repassar o erro, etc.
Tratamento de exceçãoRecomendações e reflexões
raise "Out of time — next part, please!"
Parte 2 Testes automatizados
Automação de testes (1)
Refere-se ao uso de softwares especializados e à implementação de programas ou scripts (test suites)
que são usados para testar outros softwares.
Automação de testes
Automação de testes (2)
Automação de testes
Bibliotecas ou frameworks de teste
Test suite: testes escritos especificamente para a
aplicação sob teste+• Exemplos: JUnit ( Java), RSpec e
Minitest (Ruby), Jasmine (JavaScript)
• São independentes da aplicação que será testada
• Normalmente escritos na mesma linguagem que a linguagem da aplicação sob teste
• Detectar bugs no software, tanto com relação à implementação da funcionalidade
quando à especificação da funcionalidade em si
• Documentar a especificação do código (o que o código faz), incluindo edge-cases
• Garantir que o comportamento do programa não foi afetado "sem querer" entre
duas versões do código (testes de regressão)
• Testar com mais rapidez do que seria possível com pessoas, o que permite testar
com mais frequência e detectar e corrigir erros mais rapidamente
• Testar de forma mais barata — muitos tipos de teste podem ser automatizados com
facilidade
• Permitir que o código seja alterado ou experimentado com mais segurança
• Alcançar um nível de releases frequentes e praticamente automatizada (ver o
conceito de integração contínua)
Por que usar testes automatizados?
Testes automatizados — Vantagens
• Testes automatizados não permitem afirmar que um programa sempre irá funcionar
independentemente das suas entradas ou condições de execução, mas permitem
afirmar com quais entradas e em quais condições o programa funciona
• A cobertura da suite de testes (test coverage) é uma métrica que mede a quantidade
de código testada, exercitada ou coberta pela suite de testes
• Requisitos não-funcionais (usabilidade, estabilidade, segurança, desempenho, etc)
normalmente são mais difíceis de testar
• Assim como é possível especificar um software em diferentes níveis de abstração,
também é possível testar um software em diferentes níveis e sob diferentes aspectos
• Teste caixa-preta x caixa-branca
Mais sobre testes
Testes automatizados
Testes unitários
Testes de integração
Testes de aceitação
Níveis de testeDivisão normalmente aceita: 4 níveis
Testes fim-a-fim
• Verificam a funcionalidade das menores unidades que compõem o software • Em programas orientados a objeto, a unidade básica normalmente é a classe • A ideia é testar as unidades isoladamente e garantir que elas funcionam de forma
independente antes de considerar as conexões entre elas
Níveis de teste (1)
Testes unitários
Testes de integração
Testes de aceitação
Testes fim-a-fim
• Têm como objetivo testar as interfaces e interações entre os componentes • Não faz parte do escopo destes testes a integração com outros sistemas
Níveis de testeDivisão normalmente aceita: 4 níveis
Níveis de teste (2)
Testes unitários
Testes de integração
Testes de aceitação
Testes fim-a-fim
• Também são chamados de testes de sistema • O objetivo é testar o software do ponto de vista do usuário final, normalmente
testando um caso de uso. Exemplo: após fazer login pela primeira vez, o usuário
deve ser redirecionado para a tela de configuração de conta.
Níveis de testeDivisão normalmente aceita: 4 níveis
Níveis de teste (3)
Testes unitários
Testes de integração
Testes de aceitação
Testes fim-a-fim
• Normalmente são realizados no ambiente final de operação do sistema • São usados para verificar se o software atende aos critérios de aceitação
inicialmente acordados
Níveis de testeDivisão normalmente aceita: 4 níveis
Níveis de teste (4)
• Modelo tradicional de desenvolvimento (waterfall) • A fase de teste é realizada após o término da fase de desenvolvimento, por uma equipe
independente da equipe de desenvolvimento • Crítica: a fase de testes acaba sendo usada como um buffer para compensar atrasos de
fases anteriores, comprometendo os testes • Modelo ágil
• Os testes são escritos em paralelo com o restante do código da aplicação • No modelo test-driven development (TDD), os testes são escritos antes do código, de
maneira a guiar (drive) o desenvolvimento • As suites de teste são continuamente atualizadas para incluir novos casos de teste, e se
tornam os testes de regressão da aplicação • Crítica: este modelo aumenta o esforço dedicado ao desenvolvimento e manutenção de
testes
Modelos de desenvolvimentoAs diferentes formas de enxergar o papel dos testes
Modelos de desenvolvimento
• RSpec: biblioteca open-source de testes para Ruby • A ideia é construir especificações executáveis do seu código • Divisão em 4 partes:
• rspec-core: http://rspec.info/documentation/3.2/rspec-core/ • rspec-expectations: http://rspec.info/documentation/3.2/rspec-expectations/ • rspec-mocks (não vamos usar hoje) • rspec-rails (não vamos usar hoje)
Testes em Ruby com RSpec
Testes em Ruby com RSpec
RSpecEstrutura básica de um arquivo de teste (something_spec.rb)
RSpec.describe Stack do
describe '#pop' do
context 'when the stack is empty' do
it 'raises EmptyStack exception' do
# Actual test goes here
end
end
context 'when the stack is not empty' do
it 'returns the first element of the stack' do
# Actual test goes here
end
end
end
end
• it, specify e example são usados para declarar exemplos de como o seu código deve funcionar
• São sinônimos, você pode usar a palavra que permite a leitura melhor do código
• describe e context (sinônimos) são usados para agrupar e aninhar exemplos.
• Grupos de exemplos tornam a suite de testes mais organizada e fácil de entender
RSpec — estrutura básica
RSpecExecução de um arquivo de teste
$ rspec stack_spec.rb --format documentation --color
Stack
#pop
when the stack is empty
raises EmptyStack exception
when the stack is not empty
returns the first element of the stack
Finished in 0.00114 seconds (files took 0.11435 seconds to load)
2 examples, 0 failures
Os testes funcionam como uma documentação executável
RSpec — exemplo de saída de sucesso
RSpecExecução de um arquivo de teste com falhas
$ rspec stack_spec.rb --format documentation --color Stack #pop when the stack is empty raises EmptyStack exception (FAILED - 1) when the stack is not empty returns the first element of the stack Failures: 1) Stack#pop when the stack is empty raises EmptyStack exception Failure/Error: expect {}.to raise(EmptyStack) EmptyStack: EmptyStack # ./stack_spec.rb:52:in `block (4 levels) in <top (required)>' Finished in 0.00133 seconds (files took 0.39144 seconds to load)2 examples, 1 failure Failed examples: rspec ./stack_spec.rb:51 # Stack#pop when the stack is empty raises EmptyStack exception
RSpec — exemplo de saída com falhas
RSpec Expectations
• O módulo de expectations do RSpec (RSpec::Expectations) contém diversos matchers que permitem expressar, dentro de um exemplo, as saídas esperadas do programa
• Os matchers definidos pelo RSpec estão documentados neste link: http://rspec.info/documentation/3.2/rspec-expectations/
• Também é possível definir matchers customizados
RSpec Expectations
Contém matchers para implementar os testes
RSpec ExpectationsAlguns exemplos de matchers
# Equivalence
expect(actual).to eq(expected) # passes if actual == expected
# Comparisons
expect(actual).to be > expected # also >=, <= and <
# Regular expressions
expect(actual).to match(/expression/)
# Types/classes
expect(actual).to be_a(expected) # passes if actual.is_a?(expected)
RSpec Expectations — Exemplos de matchers (1)
RSpec ExpectationsAlguns exemplos de matchers
# Truthiness
expect(actual).to be_truthy # passes if actual is truthy (not nil or false)
expect(actual).to be_falsy # passes if actual is falsy (nil or false)
expect(actual).to be_nil # passes if actual is nil
expect(actual).to_not be_nil # passes if actual is not nil
# Expecting errors
expect { ... }.to raise_error
expect { ... }.to raise_error(ErrorClass)
# Collection membership
expect(actual).to include(expected)
expect(actual).to start_with(expected)
expect(actual).to end_with(expected)
RSpec Expectations — Exemplos de matchers (2)
RSpec.describe 'Exceptions in Ruby' do
specify '`raise` without a class raises RuntimeError' do
expect { raise "Raising an error" }.to raise_error(RuntimeError)
end
specify 'RuntimeError inherits from Exception' do
expect(RuntimeError.ancestors).to include(Exception)
end
class SomeError < RuntimeError; end
specify 'you can create a new error type' do
expect { raise SomeError, 'This is my new type' }.to(
raise_error(SomeError))
end
specify 'exception objects carry call stack information with them' do
begin
raise SomeError, "I'm raising an error just to test stuff"
rescue SomeError => ex
expect(ex.backtrace).to be_a(Array)
end
end
end
RSpec — Um exemplo completo
Fonte: http://xkcd.com/1319/
Automation