Última atualização significante: 02 de Janeiro de 2007
O termo 'Mock Objects' tornou-se popular para descrever um caso especial de objetos que imitam objetos reais para teste. A maioria das linguagens de ambiente agora tem frameworks que facilitam a criação de mock objects. O que muitas vezes não é realizado, entretanto, é o que mock objects são, mas uma forma de objeto de teste em um caso especial, uma forma que permite um estilo diferente de teste. Neste artigo eu explicarei como os mock objects funcionam, como eles fazem testes baseado na verificação de comportamento e como a comunidade em torno dele usam para desenvolver um estilo diferente de teste.
Para ler o artigo original escrito por Martin Fowler clique aqui.
- Testes Regulares
- Testes com Mock Objects
- Usando EasyMock
- A diferença entre Mocks e Stubs
- Teste Clássico e Mockista
- Escolhendo Entre as Diferenças
- TDD Dirigido
- Fixture Setup
- Isolamento de Teste
- Acoplamento dos Testes à Implementação
- Estilo de Design
- Então eu devo ser um classicista ou um mockista?
- Considerações finais
- Leitura Complementar
- Histórico de Revisão
A primeira vez que me deparei com o termo 'mock object' foi há alguns anos na comunidade XP. Desde então, meu contato com mock objects só tem se intensificado. Um pouco pelo fato de que muitos dos principais desenvolvedores de mock objects foram meus colegas na ThoughtWorks em vários momentos. Um pouco também porque tenho visto mock objects cada vez mais frequentemente nas literaturas sobre testes com base em XP.
Mas apesar disso, no geral eu tenho visto poucas boas descrições sobre mock objects. Em particular, vejo que eles muitas vezes são confundidos com stubs - objetos para auxiliar no testes de ambientes. Eu compreendo esta confusão - eu mesmo os achei bem semelhantes por um tempo também, mas algumas conversas com os desenvolvedores de mocks me possibilitaram uma compreensão maior sobre o assunto.
A diferença é, na verdade, duas diferenças separadas. Por um lado há a diferença de como os resultados dos testes são verificados: uma distinção entre verificação de estado e verificação de comportamento. Por outro lado está toda uma diferença filosófica sobre como as atividades de testes e design interagem entre si, que eu costumo chamar de estilos clássico e mockista de Test Driven Development.
(Em uma primeira versão deste artigo, eu já tinha noção dessas diferenças mas combinei as duas juntas. Como então aprimorei melhor meu entendimento sobre isto, é hora de atualizar este artigo. Se você não tiver lido a versão anterior, apenas ignore estas considerações, pois eu escrevi este artigo como se sua versão anterior não existisse. Mas se tiver tido contato com a versão anterior deste artigo, você pode achar interessante ver que eu quebrei a antiga dicotomia de testes baseados em estados e testes baseados em interação na dicotomia de verificação de estado/comportamento e na de TDD clássico/mockist. Também ajustei meu vocabulário para corresponder com o do livro xUnit patterns de Gerard Meszaro.
Testes Regulares
Vou ilustrar estes dois estilos com um exemplo simples. (É um exemplo em Java, mas os princípios são aplicáveis a qualquer linguagem orientada a objetos.) Queremos obter um objeto Order (Pedido) e preenchê-lo a partir de um objeto Warehouse (Estoque). Pedido é um objeto muito simples, com apenas um único produto e uma quantidade. Já um objeto deposito contém conjuntos de diferentes produtos. Quando solicitamos a um objeto pedido que preencha a si mesmo a partir de um objeto estoque, há então duas situações possíveis. Se o estoque contiver produtos o suficiente para preencher o pedido, o pedido é preenchido e o total de produtos no estoque é reduzido pela quantidade em questão. Se não houver produtos no estoque, então o pedido não é preenchido e nada acontece com o estoque.
Estes dois comportamentos demandam um par de testes, parecidos com os testes convencionais do JUnit.
public class OrderStateTester extends TestCase { private static String TALISKER = "Talisker"; private static String HIGHLAND_PARK = "Highland Park"; private Warehouse warehouse = new WarehouseImpl(); protected void setUp() throws Exception { warehouse.add(TALISKER, 50); warehouse.add(HIGHLAND_PARK, 25); } public void testOrderIsFilledIfEnoughInWarehouse() { Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled()); assertEquals(0, warehouse.getInventory(TALISKER)); } public void testOrderDoesNotRemoveIfNotEnough() { Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled()); assertEquals(50, warehouse.getInventory(TALISKER)); }
Testes no padrão xUnit seguem uma sequência típica de quatro fases: configura, exercita, verifica, finaliza. Neste caso, a fase de setup é feita parcialmente pelo método setUp (configurando o estoque) e parcialmente pelo método de teste (configurando o pedido). A chamada à order.fill
é a fase de exercício. É aqui onde definimos o objeto a fazer aquilo que queremos testar. As declarações assert então são a fase de verificação, conferindo se o método exercitado executou sua tarefa corretamente. Neste caso, não há uma fase de finalização explícita, o coletor de lixo do ambiente (garbage collector) faz tudo para nós implicitamente.
Durante a fase de setup há dois tipos de objetos que estamos colocando juntos. Pedido é a classe que estamos testando, mas para que a chamada à order.fill
funcione, precisamos também de uma instância de um estoque. Nesta situação, o Pedido é o objeto que é o foco do teste. Pessoas orientada a testes gostam de usar termos como object-under-test ou system-under-test nesses casos. São dois são termos um pouco rebuscados, mas como são largamente usados, então também aceito passivamente usá-los. De acordo com Meszaros, irei usar System Under Test
ou a abreviação SUT.
Então, para este teste eu preciso do SUT (Order
) e de um colaborador (warehouse
). Preciso do warehouse por dois motivos: o primeiro é fazer com que o comportamento testado funcione como um todo (uma vez que order.fill
chama métodos do warehouse, e o segundo é que ele é necessário para verificação (já que um dos efeitos da chamada a order.fill
é uma potencial alteração no estado de warehouse). Conforme explorarmos este tópico ainda mais à frente, você vai ver que vamos fazer uma grande distinção entre SUT e colaboradores. (Na versão anterior deste artigo eu me referia ao SUT como "objeto primário" e aos colaboradores como "objetos secundários")
Este estilo de teste usa verificação de estado: o que significa que nós determinamos se o método exercitado funcionou corretamente examinando o estado do SUT e de seus colaboradores depois da execução do método. Como veremos, mock objects permitem uma abordagem diferente de verificação.
Testes com Mock Objects
Agora eu vou introduzir um comportamento e usar os mock objects. Nesse código, estou usando a biblioteca jMock, para java, para definir os mocks. Existem outras bibliotecas para mocks, mas essa é uma bem atualizada e que foi escrita pelos idealizadores dessa técnica. Assim, é uma boa opção para começar.
public class OrderInteractionTester extends MockObjectTestCase { private static String TALISKER = "Talisker"; public void testFillingRemovesInventoryIfInStock() { //setup - data Order order = new Order(TALISKER, 50); Mock warehouseMock = new Mock(Warehouse.class); //setup - expectations warehouseMock.expects(once()).method("hasInventory") .with(eq(TALISKER),eq(50)) .will(returnValue(true)); warehouseMock.expects(once()).method("remove") .with(eq(TALISKER), eq(50)) .after("hasInventory"); //exercise order.fill((Warehouse) warehouseMock.proxy()); //verify warehouseMock.verify(); assertTrue(order.isFilled()); } public void testFillingDoesNotRemoveIfNotEnoughInStock() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); warehouse.expects(once()).method("hasInventory") .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); assertFalse(order.isFilled()); }
Concentre-se primeiro em testFillingRemovesInventoryIfInStock
, já que eu peguei alguns atalhos com o último teste.
Para começar, a fase de setup é muito diferente. Ela está dividida em duas partes: dados e expectativas. A parte de dados ajusta os objetos com os quais nós estamos interessados em trabalhar. Nesse sentido, ela é semelhante ao setup tradicional de dados. A diferença está nos objetos criados. O SUT é o mesmo - um pedido. Entretanto o colaborador não é um objeto estoque. É um mock do estoque - tecnicamente é uma instância da classe Mock
.
A segunda parte do setup cria as expectativas sobre o mock object. As expectativas indicam quais métodos deveriam ser chamados nos mocks quando o SUT é exercitado.
Quando todas as expectativas estiverem codificadas eu exercito o SUT. Depois do exercício eu faço a verificação, que tem dois aspectos. Eu rodo os asserts no SUT - da mesma maneira que antes. Entretanto eu verifico também os mocks - checando se eles foram chamados de acordo com as expectativas.
A principal diferença aqui é como nós verificamos se o pedido fez a coisa certa em sua interação com o estoque. Com a verificação de estado nós fazemos isso pelos asserts contra o estado do estoque. Os mocks usam a verificação de comportamento, onde nos certificamos que o pedido fez as chamadas corretas ao estoque. Nós fazemos essa checagem dizendo ao mock o que esperamos durante a fase de setup e pedindo para ele próprio verificar durante a verificação. Apenas o pedido é checada usando os asserts, e se o método não muda o estado do pedido, então não existe nenhum assert.
No segundo teste eu faço algumas coisas diferentes. Primeiro eu crio o mock de um jeito diferente, usando o método mock
em MockObjectTestCase ao invés de no construtor. Este é um método na biblioteca jMock para nossa conveniência, que significa que eu não vou precisar chamar explicitamente o método verify mais tarde. Qualquer mock criado por esse método é automaticamente verificado ao final do teste. Eu deveria ter feito isso no primeiro teste também, mas eu preferi mostrar a verificação mais explicitamente para ver como funcionam os testes com mocks.
A segunda diferença no segundo caso de teste é que eu relaxei as restrições na expectativa usando withAnyArguments
. A razão disso é que o primeiro teste checa se o número foi passado para o estoque. Então o segundo teste não precisa repetir esse item do teste. Se a lógica do pedido precisar mudar mais tarde, então apenas um teste vai falhar, facilitando o esforço de ajustar os testes. Como você pode perceber, eu poderia ter deixado withAnyArguments
completamente de fora, já que assim é o default.
Usando EasyMock
Há um bom número de bibliotecas de mock object por aí. Um que eu tenho visto razoavelmente é o EasyMock, em versões java e .NET. EasyMock também possibilita verificação de comportamento, mas tem algumas diferenças em estilo com jMock que valem a pena discutir. Aqui estão os testes familiares novamente:
public class OrderEasyTester extends TestCase { private static String TALISKER = "Talisker"; private MockControl warehouseControl; private Warehouse warehouseMock; public void setUp() { warehouseControl = MockControl.createControl(Warehouse.class); warehouseMock = (Warehouse) warehouseControl.getMock(); } public void testFillingRemovesInventoryIfInStock() { //setup - data Order order = new Order(TALISKER, 50); //setup - expectations warehouseMock.hasInventory(TALISKER, 50); warehouseControl.setReturnValue(true); warehouseMock.remove(TALISKER, 50); warehouseControl.replay(); //exercise order.fill(warehouseMock); //verify warehouseControl.verify(); assertTrue(order.isFilled()); } public void testFillingDoesNotRemoveIfNotEnoughInStock() { Order order = new Order(TALISKER, 51); warehouseMock.hasInventory(TALISKER, 51); warehouseControl.setReturnValue(false); warehouseControl.replay(); order.fill((Warehouse) warehouseMock); assertFalse(order.isFilled()); warehouseControl.verify(); } }
EasyMock usa uma metáfora record/replay para definir expectativas. Para cada objeto que você quiser fazer mock você cria um control e um mock object. O mock satisfaz a interface do objeto secundário, o control te dá características adicionais. Para indicar uma expectativa você chama o método, com os argumentos que você espera no mock. A seguir você chama o control se quiser um valor de retorno. Uma vez que você terminou de definir expectativas você chama replay no control - neste ponto o mock termina a gravação e está pronto para responder ao objeto primário. Uma vez pronto você chama verify no control.
Parece que enquanto as pessoas normalmente tem receio em um primeiro momento pela metáfora record/replay, elas rapidamente se acostumam com ela. Ela tem uma vantagem sobre as restrições do jMock dado que você está fazendo chamadas reais de método para o mock ao invés de especificar nomes de método em strings. Isso significa que você pode usar code-completion na sua IDE e qualquer refactoring de nome de método vai automaticamente atualizar os testes. A desvantagem é que você não pode ter as restrições mais permissivas.
Os desenvolvedores do jMock estão trabalhando em uma nova versão que vai usar outras técnicas para permitir o uso de chamadas reais aos métodos.
A diferença entre Mocks e Stubs
Assim que surgiram, muitas pessoas confundiram os mock objects com a noção padrão da utilização de stubs para testes. Desde então parece que todos tinham um melhor entendimento das diferenças (e espero que a versão mais recente deste artigo tenha ajudado). Entretanto, para compreender completamente a maneira de se utilizar mocks é importante entender mocks e outros tipos de doubles testes. ("doubles"? Não se incomode se este é um novo termo para você, espere alguns parágrafos e tudo ficará claro.)
Quando está aplicando testes como estes, você está se concentrando em um elemento do software por vez - por isso o termo usual testes unitários. O problema é que pra fazer uma unidade funcionar, também é preciso das outras - então a necessidade de algum tipo de estoque no nosso exemplo.
Nos dois estilos de testes que mostrei abaixo, o primeiro caso usa um objeto real de estoque e o segundo utiliza um estoque mock, o que é claro, não é um objeto real. Utilizar mocks é uma maneira de não utilizar um estoque real no teste, mas existem outras formas de utilizar objetos fictícios como este.
O vocabulário para falar sobre isto logo fica confuso, todos os tipos de palavras são utilizados: stubs, mock, fake, dummy. Para este artigo vou seguir o vocabulário do livro de Gerard Meszaros. Não é o que todos utilizam, mas o acho um bom vocabulário e já que sou o escritor, escolherei quais palavras usar.
Meszaros utiliza o termo Test Double como um termo genérico para qualquer objeto falso, utilizado no lugar de um objeto real, para propósitos de testes. O nome veio dos dublês (Stunt Double) dos filmes. (Um de seus objetivos era evitar utilizar qualquer nome que já tenha sido amplamente utilizado.) Então ele definiu quatro tipos de dublês:
- Dummy Objects são repassados mas nunca utilizados. Normalmente são usados para preencher listas de parâmetros.
- Fake Objects têm implementações funcionais, mas normalmente utilizam algum atalho que os torna inadequados para produção (uma base de dados em memória é um bom exemplo.
- Stubs providenciam respostas pré configuradas para as chamadas feitas durante os testes, normalmente não respondem a nada que não esteja programado para o teste. Stubs também podem gravar informações sobre as chamadas, como um gateway que lembra as mensagens que 'enviou', ou talvez apenas quantas mensagens 'enviou'.
- Mocks é sobre que estamos falando aqui: objetos pré-programados com informações que formam uma especificação das chamadas que esperam receber.
Destes tipos de dublês, apenas os mocks insistem na verificação do comportamento. Os outros podem, e normalmente utilizam verificação de estados. Mocks se comportam como os outros na fase de exercício, já que precisam fazer o SUT acreditar que estão falando com seus reais colaboradores - mas diferem nas fases de setup e verify.
Para explorar melhor os dublês, precisamos estender nosso exemplo. Muitas pessoas os utilizam apenas se o objeto real não está disponível. Um caso mais comum para o teste com dublês seria se disséssemos que gostaríamos de enviar um email se falhássemos ao preencher um pedido. O problema é que não queremos enviar mensagens para clientes durante os testes. Assim, em vez disso criaremos um dublê para nosso sistema de emails, um que possamos controlar e manipular.
Aqui começamos a notar as diferenças entre mocks e stubs. Se estivéssemos escrevendo um teste para este comportamento de envio de mensagens, poderíamos utilizar um stub tão simples quanto este:
public interface MailService { public void send (Message msg); } public class MailServiceStub implements MailService { private Listmessages = new ArrayList ();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
E então poderíamos utilizar verificação de estados no stub assim:
class OrderStateTester... public void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); MailServiceStub mailer = new MailServiceStub(); order.setMailer(mailer); order.fill(warehouse); assertEquals(1, mailer.numberSent()); }
Claro que é um teste muito simples - verificando se a mensagem foi enviada. Não verificamos se o foi para a pessoa certa ou se estava com o conteúdo correto, mas servirá para ilustrar o exemplo.
Ao utilizar mocks o teste terá uma forma um pouco diferente.
class OrderInteractionTester... public void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); Mock mailer = mock(MailService.class); order.setMailer((MailService) mailer.proxy()); mailer.expects(once()).method("send"); warehouse.expects(once()).method("hasInventory") .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); } }
Nos dois casos estou utilizando um teste dublê no lugar de um serviço real de email. A diferença está no fato do stub usar verificação de estados enquanto o mock verifica comportamento.
Para que o stub faça a verificação de estados, preciso escrever alguns métodos extras. Como resultado ele implementa MailService mas possui mais métodos de testes.
Mock Objects sempre utilizam verificação de comportamento, um stub pode seguir o mesmo caminho. Meszaros chama os stubs que utilizam este tipo de verificação como Teste Espião. A diferença está em como, exatamente, o dublê roda e efetua a verificação e deixarei esta descoberta para você.
Teste Clássico e Mockista
Agora estou no ponto onde posso explorar a segunda dicotomia: que é entre o TDD clássico e o mockista. O X da questão aqui é quando utilizar um mock (ou outro similar).
O estilo do TDD clássico utiliza objetos reais quando possível e um similar quando não há condições de se utilizar o objeto real. Desta maneira, um TDDer clássico poderia utilizar um objeto estoque real e um similar para o serviço de email. O tipo do similar na verdade não interessa tanto.
Um praticante do TDD mockista, por outro lado, irá sempre utilizar um mock para qualquer objeto que tenha um comportamento interessante. Neste caso, irá utilizar um mock tanto para o estoque quanto para o serviço de email.
Apesar de vários frameworks para mock terem sido projetados como o tipo de teste mockista em mente, muitos classistas consideram estes úteis para a criação de objetos similares.
Uma derivação importante do estilo mockista é o do Behavior Driven Development (BDD). BDD foi desenvolvido originalmente pelo meu colega Dan North como uma técnica para ajudar as pessoas a aprender Test Driven Development, focando no como o TDD opera como uma técnica de design. O que nos leva a renomear testes para comportamentos, de maneira a explorar melhor onde o TDD ajuda com o pensamento do quê efetivamente o objeto precisa. BDD toma uma abordagem mockista, mas ele vai além disto, tanto com seus estilos de nomenclatura quanto com seus impulsos de integrar análise junto á técnica. Neste artigo não vou muito além disto, sendo que a única relevância para este artigo é que o BDD é uma outra variação de TDD que tende a utilizar testes mockista. Vou deixar para você seguir o link para maiores informações.
Escolhendo Entre as Diferenças
Neste artigo expliquei um conjunto de diferenças: verificação do estado ou de comportamento / TDD clássico ou mockista. Quais são os argumentos que temos de ter em mente ao fazer a escolha entre eles? Vou iniciar com a escolha da verificação do estado versus comportamento.
A primeira coisa a considerar é o contexto. Nós estamos pensando sobre uma associação fácil, como pedido e estoque, ou uma mais complicada, como pedido e serviço de email?
Se for uma associação simples então a escolha é fácil. Se eu sou um TDDer clássico eu não utilizo mock, stub ou qualquer outro similar. Eu utilizo um objeto real e verificação de estado. Se sou um TDDer mockista eu utilizo um mock e verificação de comportamento. Sem decisões.
Se for uma associação incomum, então não há decisão uma ser tomada, se sou um mockista – Eu utilizo somente mocks e verificação de comportamento. Se sou um classicista então ainda tenho uma escolha, não que isso seja um bom negócio. Geralmente classicistas irão decidir baseados caso a caso, utilizando a rota mais fácil para cada situação.
Como nós vimos, verificação de estado vs comportamento nem sempre é um grande decisão. O maior problema é entre o TDD clássico e o mockista. A partir do momento que as características da verificação de estado e comportamento começarem a afetar a discussão, então é aí que focarei grande parte de minha energia.
Mas antes que eu faça, permita-me analisar um caso extremo. Ocasionalmente você inicia atividades que são realmente difíceis de utilizar verificação de estado, mesmo que não sejam associações complexas. Um grande exemplo disto é o cache. A grande questão sobre cache é que você não pode dizer qual estado o cache atingiu ou perdeu – este é um caso onde a verificação de comportamento seria uma escolha sábia mesmo para um TDDer clássico extremista. Tenho certeza que existem outras exceções em ambas direções.
Conforme nós nos aprofundamos na escolha de clássico/mockista, existem uma série de fatores que devemos considerar, de maneira que tenho quebrado estes fatores em grupos bem rudimentares.
TDD Dirigido
Mock Objects vieram da comunidade XP, e uma das funcionalidades principais do XP é a sua ênfase em Test Driven Development – onde o design do sistema é evoluído através de iterações dirigidas por escrita de testes.
Portanto, não é surpresa nenhuma que os mockistas em particular falam sobre o efeito de testes com mocks no design. Em particular, eles defendem um estilo chamado desenvolvimento dirigido à necessidade. Com este estilo, você inicia o desenvolvimento de uma estória escrevendo primeiro um teste para o lado externo do seu sistema, fazendo um objeto de interface para seu SUT. Pensando na expectativa dos colaboradores, você explora a interação entre o SUT e seus vizinhos – fazendo o design efetivo da interface de saída do SUT.
Uma vez com o primeiro teste rodando, as suposições dadas pelos mocks forneçam um descritivo para o próximo passo e um ponto inicial para os testes. Você torna cada suposição em um teste helper e repete o processo trabalhando na sua maneira para atender o sistema, com um SUT por vez. Este estilo é também referenciado como "de fora para dentro", que é um nome muito descritivo para esta função. E trabalha muito bem com sistemas multi camadas. Você inicia primeiro programando a UI utilizando camadas mock por baixo. Então escreve os testes para a camada mais baixa, e vai subindo gradualmente pelo sistema uma camada por vez. Esta é uma abordagem muito controlada e estruturada, uma abordagem que muitas pessoas acreditam ser útil para auxiliar os novatos em OO e TDD.
O TDD clássico não fornece a mesma direção. Você pode adotar uma abordagem similar, usando métodos stub ao invés de mocks. Para fazer isto, sempre que precisa de algo de um helper, você faz um hard-code exato da saída que o teste requer para fazer o SUT funcionar. Então, uma vez que o teste do código está verde, você substitui a saída hard coded com o código apropriado.
Mas TDD clássico pode fazer outras coisas também. Um estilo comum é o "do meio para fora". Neste estilo você pega uma funcionalidade e decide o que é preciso no domínio para que esta funcionalidade funcione. Você pega os objetos de domínio para fazer o que precisa e uma vez que eles estão funcionando, você inclui a camada de UI por cima. Fazendo isto, talvez você nunca precise simular nada. Muitas pessoas gostam disto porque foca a atenção no modelo de domínio primeiro, o que ajuda a prevenir que a lógica do domínio se misture com a UI.
Eu deveria enfatizar que ambos mockistas e classicistas fazem isto com uma estória por vez. Existe uma linha que constrói as aplicações camada por camada, não iniciando uma camada até que a outra esteja completa. Tanto classicistas quanto mockistas tendem a ter um background agile e preferem iterações de baixa-granularidade. Como resultado, eles trabalham funcionalidade por funcionalidade ao invés de camada por camada.
Fixture Setup
Com TDD clássico, você tem que criar não só uma SUT, mas também todas as colaborações que a SUT precisa em resposta ao teste. Embora o exemplo só tinha um par de objetos, teste reais envolvem freqüentemente um grande número de objetos secundários. Geralmente esses objetos são criados e destruídos a cada execução de testes.
Testes com mock, entretanto, apenas precisam criar o SUT e mocks para os vizinhos imediatos. Isso pode evitar alguns dos trabalhos envolvidos na construção de uma fixture complexa (Pelo menos na teoria. Tenho visto setups de mocks muito complexos, mas é devido ao mau uso das ferramentas.)
Na prática, testadores clássicos tendem a reusar o máximo possível as fixtures complexas. Na forma mais simples você faz isso colocando código de setup de fixtures dentro do método de setup xUnit. Fixtures mais complicadas precisam ser usadas por várias classes de testes, então nesse caso você cria classes geradas para uma fixture especial. Eu geralmente chamo esses de Object Mothers, baseado na convenção de nome usado recentemente em um projeto XP da ThoughtWorks. Usar mothers é essencial na maioria dos testes clássicos, mas mothers são códigos adicionais que necessitam ser mantidos em qualquer mudança que tenha efeito cascata nos testes. Também pode haver um custo de performance em configurar uma fixture - embora eu não tenha ouvido falar que esse é um problema sério quando feito corretamente. Muitos objetos fixtures são baratos para criar, aqueles que não são geralmente duplicados.
Como um resultado eu ouvi os dois estilos acusando o outro de ter muito trabalho. Mockistas dizem que criação de fixtures é um grande esforço, mas os clássicos dissem que esse é reusado mas você tem que criar mocks com todos testes.
Isolamento de Teste
Se você introduz um bug em um sistema com testes mocados, geralmente irá causar problemas nos testes que a SUT contém o bug para falhar. Com a abordagem clássica, entretanto, qualquer teste de objetos clientes podem também falhar, o que leva a falhas quando o objeto bugado é usado em colaboração com outro objeto de teste. Como um resultado uma falha em um objeto muito usado causa uma onda de falhas nos testes em todo o sistema.
Testers que usam mocks consideram essa ser a maior questão; isso resulta em muitos debugging para achar a causa raiz do erro e corrigí-lo. Entretanto, os clássicos não expressam isso como uma fonte do problema. Geralmente o culpado é relativamente fácil de detectar, olhando para os testes que falham e os desenvolvedores podem dizer que outras falhas são derivadas do erro inicial. Além disso se você está testando regularmente (como deveria) então você sabe que a ruptura foi causada pelo que você editou por último, então não é difícil achar a falha.
Um fator que pode ser significante aqui é a granularidade dos testes. Já que os testes clássicos exercitam multiplos objetos reais, você geralmente encontra um único teste como um teste primário para um cluster de objetos, e não apenas um. Se o cluster abrange muitos objetos, então pode ser muito mais difícil de encontrar a fonte real de um bug. O que está acontecendo aqui é que os testes estão com uma granularidade muito grosseira.
É bastante provável que testes com mock são menos propensos a sofrer com esse problema, porque a convensão é mocar todos objetos além da primária, o que deixa claro que testes de granularidade fina são necessários para a colaboradores. Dito isso, é verdade também que o uso de testes excessivamente granulados não é necessariamente uma falha de testes clássicos como uma técnica, e sim uma falha de fazer testes clássicos propriamente. Uma boa regra é assegurar que você separa testes de granularidade fina para todas as classes. Enquanto clusters são as vezes razoáveis, eles devem ser limitados a muito poucos objetos - não mais que meia dúzia. Em adição, se você se encontra debugando um problema devido a testes de granularidade alta, você deve debugar de uma maneira dirigido a teste, criando testes de granularidade fina.
Na essencia testes xunit classicos não são somente testes de unidade, mas também um mini-teste de intergração. Como um resultado muitas pessoas gostam do fato de que testes clientes podem capturar erros que os testes principais para um objeto pode ter perdido, particularmente sondando áreas onde classes interagem. Testes com mock perdem qualidade. Em adição você corre o risco que expectativas nos testes com mock podem estar incorretos, resultando em testes de unidade que passam verde mas mascaram erros inerentes.
É nesse ponto que devo salientar que qualquer estilo de teste que você use, você deve combinar com testes de aceitação que operam no sistema inteiro. Eu geralmente venho de projetos que usaram tarde testes de aceitação e se arrependeram.
Acoplamento dos Testes à Implementação
Quando você escreve um teste mockista, você testa as chamadas que o sistema que está sendo testado (SUT) realiza para garantir que ele se comunica corretamente com seus colaboradores. Um teste clássico só se preocupa com o estado final - e não com como esse estado foi atingido. Desta forma, testes mockistas são mais acoplados a como um método é implementado. Mudanças na natureza das chamadas para os colaboradores costumam fazer com que testes mockistas falhem.
Este acoplamento gera uma série de preocupações. A mais importante é o efeito sobre o test driven development (TDD). Com testes mockistas, você começa a pensar sobre a implementação do comportamento enquanto ainda está escrevendo o teste - na verdade, testadores mockistas vêem isso como uma vantagem. Testadores clássicos, no entanto, defendem que é importante pensar apenas sobre o que acontece de um ponto de vista externo e deixar as considerações sobre implementação para depois de terminar de escrever o teste.
O acoplamento com a implementação também interfere na hora do refactoring, uma vez que mudanças na implementação são muito mais propensas a quebrar testes mockistas do que testes clássicos.
Isso pode ser agravado pela natureza das ferramentas de mock. Muitas vezes as ferramentas de mock especificam chamadas de método e passagem de parâmetros muito específicos, mesmo quando essas coisas não são relevantes para aquele teste em particular. Um dos objetivos do conjunto de ferramentas jMock é ser mais flexível na especificação das expectativas para permitir que as expectativas possam ser mais "relaxadas" quando elas não importam muito. A desvantagem é a utilização de strings para tal tarefa, o que pode complicar o refactoring.
Estilo de Design
Para mim, um dos aspectos mais fascinantes desses estilos de teste é como eles afetam as decisões de design. Enquanto eu falei sobre os dois tipos de testador, percebi algumas diferenças entre os diferentes designs que cada estilo incentiva, mas eu tenho certeza que essa análise é bastante superficial.
Eu já mencionei uma diferença de como cada testador lida com camadas. Testes mockistas incentivam uma abordagem de fora pra dentro, enquanto os desenvolvedores que preferem um estilo iniciando com o modelo de domínio tendem a preferir testes clássicos.
Olhando mais de perto, notei que os testadores mockistas tendem a evitar o uso de métodos que retornam valores, usando em vez disso métodos que atuam sobre um objeto coletor. Tomemos como exemplo um sistema que coleta informações de um grupo de objetos para criar uma string de relatório. Uma maneira comum de fazer isso é fazer com que o método de geração de relatório faça chamadas a métodos que retornam strings dos vários objetos e montar a string de relatório em uma variável temporária. Um testador mockista provavelmente iria passar um string buffer para os vários objetos e fazê-los adicionar as diversas strings a esse buffer - tratando o string buffer como um parâmetro de coleta.
Testadores mockistas focam mais em como evitar "acidentes de trem" - cadeias de métodos como getThis().getThat().getTheOther()
. Evitar cadeias de métodos também é conhecido como seguir a lei de Demeter. Embora as cadeias de método sejam um indício de código ruim, o problema oposto de usar objetos intermediários inchados com métodos de encaminhamento para métodos de variáveis internas é também um indício de que o código está ruim. (Eu sempre achei que me sentiria mais confortável com a Lei de Demeter se ela fosse chamada de Sugestão de Demeter).
Uma das coisas mais difíceis para as pessoas entenderem em design orientado a objetos é o princípio "Diga, não Pergunte", que encoraja a dizer um objeto o que fazer ao invés de extrair dados de um objeto para executar a tarefa no código cliente. Os mockistas afirmam que o uso de testes mockistas ajuda a promover este princípio e a evitar a "festa de getters" que permeia muito do código esses dias. Os clássicos argumentam que existem muitas outras maneiras de fazer isso.
Um problema conhecido relacionado à verificação baseada em estado é que ela pode levar à criação de métodos de consulta apenas para apoiar a verificação. Não é nada confortável adicionar métodos à API de um objeto apenas para propósitos de teste, e fazer a verificação com base no comportamento evita esse problema. O contra-argumento é que essas alterações são geralmente pequenas na prática.
Os mockistas preferem interfaces baseadas em papéis e afirmam que o uso deste estilo de teste incentiva isso, já que cada colaboração é mocada separadamente e, consequentemente, mais suscetível a ser transformada em uma interface baseada em papel. Então, no meu exemplo acima usando um string buffer para gerar um relatório, um mockista provavelmente criaria um papel especial que faz sentido nesse domínio, e que pode ser implementado usando um string buffer.
É importante lembrar que essa diferença no estilo de design é um dos maiores motivadores para a maioria dos mockistas. A origem do TDD se deu devido a um desejo de obter bons testes de regressão automática que dessem suporte a um design evolutivo. Ao longo do caminho seus praticantes descobriram que escrever os testes primeiro resultava em uma melhoria significativa no processo de design. Os mockistas ter uma opinião forte sobre que tipo de design é um bom design e desenvolveram bibliotecas de mock principalmente para ajudar as pessoas a desenvolver esse estilo de design.
Então eu devo ser um classicista ou um mockista?
Eu acho que essa é uma pergunta difícil de se responder com propriedade. Pessoalmente, eu sempre gostei de ser o clássico e antiquado praticante de TDD e até agora eu ainda não encontrei nenhuma razão para mudar. Eu não vejo nenhum benefício imperdível de ser um praticante de TDD mockista e me preocupo com as consequências de se atrelar testes à implementação.
Isso particularmente me chamou atenção quando eu observei um programador mockista. Eu realmente gosto do fato de que ao escrever os testes você foca no resultado do comportamento e não em como ele é implementado. O mockista está pensando constantemente em como o SUT será implementado para poder escrever as expectativasIsso me parece muito artificial.
Eu também sofro da desvantagem de não tentar o TDD mockista em nada além de projetos de brincadeira. Conforme eu aprendi do próprio Test Driven Development, é difícil julgar uma técnica sem tentá-la em nada sério. Eu conheço vários desenvolvedores que são mockistas bastante satisfeitos e convencidos. Então embora eu ainda seja um classicista convencido, eu preferiria apresentar o argumento de ambos os lados da forma mais justa que eu puder para que você possa se decidir por si só.
Então, se testes com mocks lhe aparece atraente, eu sugeriria tentá-lo. Vale particularmente a pena se você está tendo problemas em casos em que o TDD mockista foi feito para ajudar. Eu vejo duas situações principais aqui. Uma é quando você está constantemente debugando quando testes falham porque eles não estão quebrando de forma clara e lhe dizendo onde está o problema. (Você também pode melhorar esta situação utilizando TDD clássico ou clusters mais precisos.) A segunda situação é se seus objetos não contém comportamentos suficientes. Testes com mocks podem encorajar a equipe a criar objetos com mais comportamentos.
Considerações Finais
Conforme o interesse em testes unitários, em frameworks xunit e no Test Driven Development cresceu, mais e mais pessoas estão utilizando mocks. A todo momento as pessoas aprendem um pouco dos frameworks de mock sem entender de forma clara a divisão entre o desenvolvimento clássico e mockista que os originou. Independente de que lado você tenda, eu acho que é útil entender a diferença entre eles. Apesar de você não precisar ser um “mockista” para achar os frameworks de mock úteis, ajuda entender o pensamento que guia muitas das decisões de projeto do software.
A razão deste artigo era – e é – apontar estas diferenças e estabelecer os compromissos que se deve levar em conta em cada um. Há muito mais assunto sobre o pensamento “mockista” do que eu pude abordar aqui, particularmente as conseqüências no estilo de projeto. Eu espero que nos próximos anos nós veremos mais material sobre isso e que irá aprofundar nosso entendimento das conseqüências fascinantes de se escrever testes antes do código.
Leitura Complementar
Para uma visão mais completa da prática de testes com frameworks xunit, acompanhe o livro de Gerard Meszaros (aviso: está na minha série). Ele também tem um website com padrões utilizados no livro.
Para aprender mais sobre o TDD, o primeiro lugar para se olhar é o livro do Kent.
Para aprender mais sobre o estilo “mockista” de se testar, o primeiro lugar para se olhar é o site mockobjects.com onde o Steve Freeman e o Nat Price advogam o ponto de vista “mockista” com papers e um blog que vale a pena. Particularmente, leia o excelente paper OOPSLA. Para mais informações sobre Behavior Driver Development, uma prática um pouco diferente do TDD que é bastante “mockista”, comece com a introdução do Dan North.
Você também pode aprender mais sobre estas técnicas ao visitar os sites das ferramentas como jMock, nMock, EasyMock e o .NET EasyMock. (Há outras ferramentas de mock, então não considere que esta lista está completa.)
O XP2000 teve o paper original sobre mocks, mas já está um pouco defasado.
Histórico de Revisão
- 02 de Janeiro de 2007:Dividi a distinção original do teste baseado em estado versus o teste baseado em interação em dois: estado versus verificação de comportamento e TDD clássico versus mockista. Eu também fiz várias mudanças no vocabulário para alinhá-lo com o livro de Gerard Meszaros de xunit patterns
- 08 de Julho de 2004: Primeira Publicação
Tradutores
A InfoQ Brasil agradece novamente à todos os editores que participaram da tradução desse artigo e àqueles que participam ativamente da tradução de notícias e artigos para o InfoQ Brasil, nesse artigo: Acyr Tedeschi, Carlos Mendonça, Marcelo Andrade, Ricardo Almeida, Ricardo Yasuda, Samuel Carrijo, Vinicius Assef e Wagner Santos. Obrigada à todos!