Fork me on GitHub

Keep Learning Conhecimento nunca é o bastante

Postado em
8 May 2009 @ 19:04

Tag(s)
Agile, Extreme Programming, Test-Driven Development

Testes devem revelar a intenção do código

Essa frase não é novidade para ninguém – ou, pelo menos, não deveria ser. No entanto, é muito mais difícil fazer isso acontecer do que falar sobre o assunto.

É muito bom que a mentalidade de testes esteja sendo cada vez mais difundida. Com isso, desenvolvem-se as abordagens às práticas de desenvolvimento orientado por testes como, por exemplo, os frameworks. Enquanto eles se tornam cada vez mais extensíveis, eficientes e com DSLs mais limpas e bonitas, muitas pessoas esquecem que, independentemente da ferramenta, da sintaxe e da abordagem técnica a ser utilizada, a intenção do código desenvolvido é que deve ficar explícita.

Observação: ao longo desse texto, utilizarei a expressão “desenvolvimento orientado por testes”, mas você pode substituir por “desenvolvimento orientado por comportamento” ou qualquer abordagem similar.

O que é a intenção do código?

A intenção do código tem tudo a ver com algo que vem sendo muito discutido: o comportamento do software. Uma definição de comportamento é “a resposta observável de um sistema a um estímulo“. Logo, em última análise, podemos dizer que o que importa é verificar se a resposta observável de um componente de software é a esperada, dado um ou mais estímulos pré-definidos.

Em alguns casos isso significa apenas definir um estado, chamar algum método e verificar o resultado. Em outros, significa verificar também a propagação dos efeitos em outros objetos (caso onde os mocks são muito úteis). Isso vai depender do componente em desenvolvimento e da abordagem utilizada.

Como?

A pergunta é: como testes ajudam a revelar a intenção do código?

Se você escrever um monte de código confuso e, após isso, resolver escrever alguns testes para validar esse trabalho, esses testes não vão ajudar em nada. Mas, se utilizar testes para orientar seu processo de desenvolvimento, descobrirá o papel fundamental que eles terão para criar código claro, fácil de entender, utilizar e modificar. Ao especificar um cenário (o estado pré-definido onde ocorrerá o estímulo), fica muito mais simples projetar e verificar o comportamento necessário.

Geralmente, ao escrever testes antes da implementação, obtêm-se outros benefícios, tais como: uso de boa nomenclatura, simplicidade e facilidade no refactoring. Tudo isso deve-se ao fato de que, ao praticar o desenvolvimento orientado por testes, você efetivamente pensa mais sobre o que vai escrever, antes de o fazer.

Exemplo

Vou tentar exemplificar os passos de criação de um componente simples com e sem testes para orientar a implementação. Assim, ao final, teremos uma perspectiva melhor dos efeitos do processo.

Quero escrever uma aplicação para cadastrar minha coleção de filmes. Preciso de uma classe que representará o filme:

class Movie
  attr_accessor :title
end

Isso é o suficiente para o exemplo.

Agora vamos iniciar o processo de implementação da classe responsável pelo gerenciamento da coleção. Primeiro, sem teste algum.

Sei que preciso de uma classe que possua uma lista de filmes e permita que eu posso adicionar, remover, procurar e listar todos os filmes. Vamos lá:

class MovieShelf
  attr_accessor :movies
 
  def initialize
    @movies = []
  end
 
  def lookup(movie)
    @movies.detect {|m| m == movie}
  end
 
  def list
    @movies.each do |movie|
      puts movie.title
    end
  end
end

Ok, muito simples. Em dois minutos está pronto. Um exemplo de uso (no irb):

>> shelf = MovieShelf.new
=> #<movieShelf:0x36eb88 @movies=[]>
>> m = Movie.new
=> #<movie:0x33b328>
>> m.title = "Juno"
=> "Juno"
>> m2 = Movie.new
=> #<movie:0x39968>
>> m2.title = "Transformers"
=> "Transformers"
>> shelf.movies << m
=> [#<movie:0x33b328 @title="Juno">]
>> shelf.movies << m2
=> [#<movie:0x33b328 @title="Juno">, #<movie:0x39968 @title="Transformers">]
>> shelf.list
Juno
Transformers
=> [#<movie:0x33b328 @title="Juno">, #<movie:0x39968 @title="Transformers">]
>> shelf.lookup m2
=> #<movie:0x39968 @title="Transformers">
>> shelf.movies.delete m2
=> #<movie:0x39968 @title="Transformers">
>> shelf.list
Juno
=> [#<movie:0x33b328 @title="Juno">]

Tudo bem, funciona e parece estar de acordo com os requisitos. Mas o código não está muito consistente. A coleção de filmes, que na prática é um objeto da classe Array, está exposta e pode ser manipulada diretamente. Isso pode ser muito ruim caso algum método da classe MovieShelf faça ações adicionais ao operar sobre a coleção. A nomenclatura também não é muito clara, entre outros pequenos problemas. Também não gostei da forma como estou criando os objetos que representam os filmes.

Agora, vamos começar com uma pequena especificação do que é preciso. Com isso, vou ter mais tempo para pensar em bons nomes e numa forma de uso limpa e direta.

context "a movie shelf" do
  setup { @shelf = MovieShelf.new }
 
  should "let the user store a movie" do
    juno = Movie.new("Juno")
    shelf.store juno
    assert shelf.contains?(juno)
  end
end

Somente estabelecendo essa primeira expectativa quanto ao comportamento do componente, já temos idéia de um início de implementação:

class Movie
  attr_accessor :title
 
  def initialize(title)
    self.title = title
  end
end
 
class MovieShelf
  def initialize
    @movies = []
  end
 
  def store(movie)
    @movies << movie
  end
 
  def contains?(movie)
    @movies.include? movie
  end
end

Continuando:

context "a movie shelf" do
  setup { @shelf = MovieShelf.new }
 
  should "show how many movies are stored" do
    juno = Movie.new("Juno")
    transformers = Movie.new("Transformers")
    shelf.store juno
    shelf.store transformers
    assert_equal 2, shelf.movies_count
  end
end

Implementando:

class MovieShelf
  def initialize
    @movies = []
  end
 
  def store(movie)
    @movies << movie
  end
 
  def contains?(movie)
    @movies.include? movie
  end
 
  def movies_count
    @movies.size
  end
end

É claro que o exemplo é bem bobo, mas perceba como a interface fica melhor e também como vamos descobrindo novas funcionalidades ao longo do processo de especificação. Este é um dos principais benefícios do desenvolvimento orientado por testes: interfaces ricas através de código modular, flexível e de intenção clara.

Não vou continuar o exemplo com as demais funcionalidades para não alongar ainda mais o artigo. Acredito que consegui expor aqui a importância da intenção do código e como deixá-la clara e fácil de entender.

O que você pensa sobre isso? Deixe sua opinião, comentário, exemplo, crítica ou sugestão. 🙂

Leia também:


3 Comentários

Comentário por
Patrick Espake
9 May 2009 @ 0:20

Legal!
Gostei. Obrigado.


Comentário por
elomarns
9 May 2009 @ 0:54

Gostei do post.

Confesso que apesar de já saber os benefícios de escrever o teste antes do código a ser testado, esse post me ajudou a visualizar isso na prática.

Mas como sou chato, não poderia deixar de perguntar: algum motivo em particular para ter usado o método detect ao invés do include??


Comentário por
Lucas Húngaro
9 May 2009 @ 3:48

@Patrick: Valeu!

@elomarns: Opa! Então, no caso do segundo código foi um resíduo mental mesmo: na primeira implementação usei o detect pra retornar o objeto ao invés de um booleano. Mas, na segunda, como o próprio ponto de interrogação sugere, deveria utilizar o include mesmo. Obrigado! 🙂


Deixe um comentário