Veja como meu código funciona. Eu tenho um objeto que representa o estado atual de algo semelhante a um pedido de carrinho de compras, armazenado em uma API de compras de terceiros. No código do meu controlador, desejo poder chamar:
myOrder.updateQuantity(2);
Para realmente enviar a mensagem para terceiros, o terceiro também precisa saber várias coisas específicas para ESTE pedido, como oe orderID
o loginID
que não serão alteradas durante a vida útil do aplicativo.
Então, quando eu crio myOrder
originalmente, injeto a MessageFactory
, o que sabe loginID
. Então, quando updateQuantity
é chamado, o Order
passa adiante orderID
. O código de controle é fácil de escrever. Outro encadeamento lida com o retorno de chamada e as atualizações, Order
se a alteração foi bem-sucedida, ou informa Order
que sua alteração falhou, se não foi.
O problema está testando. Como o Order
objeto depende de a MessageFactory
e precisa MessageFactory
retornar Message
s reais (que são chamados .setOrderID()
, por exemplo), agora tenho que configurar MessageFactory
zombarias muito complicadas . Além disso, não quero matar nenhuma fada, pois "Toda vez que um Mock devolve um Mock, uma fada morre".
Como posso resolver esse problema, mantendo o código do controlador tão simples quanto? Li esta pergunta: /programming/791940/law-of-demeter-on-factory-pattern-and-dependency-injection, mas não ajudou porque não falou sobre o problema de teste .
Algumas soluções em que pensei:
- De alguma forma, refatorar o código para não exigir que o método de fábrica retorne objetos reais. Talvez seja menos de uma fábrica e mais de uma
MessageSender
? - Crie uma implementação somente de teste
MessageFactory
e injete isso.
O código está bastante envolvido, aqui está minha tentativa de um sscce:
public class Order implements UpdateHandler {
private final MessageFactory factory;
private final MessageLayer layer;
private OrderData data;
// Package private constructor, this should only be called by the OrderBuilder object.
Order(OrderBuilder builder, OrderData initial) {
this.factory = builder.getFactory();
this.layer = builder.getLayer();
this.data = original;
}
// Lots of methods like this
public String getItemID() {
return data.getItemID();
}
// Returns true if the message was placed in the outgoing network queue successfully. Doesn't block for receipt, though.
public boolean updateQuantity(int newQuantity) {
Message newMessage = factory.createOrderModification(messageInfo);
// *** THIS IS THE KEY LINE ***
// throws an NPE if factory is a mock.
newMessage.setQuantity(newQuantity);
return layer.send(newMessage);
}
// from interface UpdateHandler
// gets called asynchronously
@Override
public handleUpdate(OrderUpdate update) {
messageInfo.handleUpdate(update);
}
}
fonte
Order
objeto e oMessageFactory
. Esta é uma boa descrição, mas é um pouco abstrato abordar diretamente com uma resposta clara.layer.send
a mensagem é enviada ou se ela está correta ?verify(messageMock).setQuantity(2)
, everify(layer).send(messageMock);
também oupdateQuantity
deve retornar false se oOrder
já tiver uma atualização pendente, mas eu omiti esse código por razões de sscce.Respostas:
A principal preocupação aqui é que as zombarias não podem (ou não devem) devolvê-las. Provavelmente, este é um bom conselho, mas trata de uma solução: devolver um real
Message
. Se aMessage
aula for bem testada e aprovada, você pode considerá-la tão amigável quanto uma farsa. Talvez seja ainda mais amigável, pois responderá como a coisa real, porque é a coisa real.Que tipo de real
Message
você pode retornar? Bem, você pode retornar um real completoMessage
, um real simplificadoMessage
(em que são usados padrões conhecidos) ou pode retornar umNullMessage
(como no Padrão de Objeto Nulo). ANullMessage
é tão válidoMessage
quanto qualquer outro e pode ser colocado em qualquer outro lugar do seu aplicativo. Qual deles usar depende da complexidade de criar e retornar uma mensagem completa.Quanto à Lei de Deméter, existem várias preocupações aqui. Primeiro, seu construtor usa seu próprio construtor como parâmetro e depois extrai elementos dele. Isso é uma clara violação de Demeter e também cria uma dependência supérflua. Pior ainda, o construtor está agindo como um mini localizador de serviço, mascarando as reais dependências da classe. O
OrderBuilder
deve criar esses objetos e passá-los como seus próprios parâmetros.Para testar isso, você passaria um mock
MessageFactory
, que retornaria um realMessage
(completo, simples ou nulo) e um mockMessageLayer
que recebesse a mensagem. Se você usar um completo ou simplificadoMessage
, poderá recuperá-lo do seuMessageLayer
mock e inspecioná-lo para testar asserções.Também gostaria de olhar para o
MessageFactory
eMessageLayer
como uma moita funcionalidade em um nível diferente de abstração, e por isso gostaria de extrair umaMessageSender
classe que encapsulado essa funcionalidade. Você pode testar esta classe usando uma simulação simplesMessageSender
e mudar tudo o que eu falei acima para osMessageSender
testes, aderindo mais à Responsabilidade Única também.Vejo que há realmente duas perguntas aqui. Há uma pergunta específica de como testar esse código e uma pergunta geral sobre zombarias que retornam zombarias. A questão específica é com o que eu lidei acima em maior medida, e tenho mais pensamentos no final daqui agora que mais alguns detalhes vieram à tona, mas ainda não há uma boa resposta para a pergunta geral: Por que as zombarias não devem retornar zombarias?
A razão pela qual as simulações não devem retorná-las é que você pode testar seus testes em vez de testar seu código. Em vez de apenas garantir que a unidade esteja totalmente funcional, o teste agora depende de um novo pedaço de código encontrado apenas no próprio caso de teste (que muitas vezes não é testado). Isso cria dois problemas.
Primeiro, o teste agora não pode me dizer com certeza se a unidade está quebrada ou se as zombarias inter-relacionadas estão quebradas. O objetivo principal de um teste é criar um ambiente isolado onde deve haver apenas uma causa de falha. Um mock-up por si só é geralmente muito simples e pode ser inspecionado diretamente quanto a problemas, mas conectar vários trocadilhos como esse se torna exponencialmente mais difícil de confirmar pela inspeção.
O segundo problema é que, à medida que as APIs mudam para os objetos reais, os testes podem começar a falhar muito longe, já que as zombarias também não são alteradas automaticamente. A Lei de Deméter entra em jogo aqui, pois esses são exatamente os tipos de efeitos que a lei evita. Nos meus testes, eu precisaria me preocupar em manter em sincronia não apenas as simulações das dependências diretas, mas também as simulações das dependências das dependências ad infinitum . Isso tem o efeito da cirurgia de espingarda nos testes quando as classes mudam.
Agora, quanto à questão específica de como testar esse trecho de código específico, vamos detalhar algumas suposições.
Pergunta 1: O que estamos realmente testando? Embora essa seja uma parte abreviada do código, podemos ver três atividades essenciais acontecendo aqui. Primeiro, temos uma fábrica gerando a
Message
. Não estamos testando se a fábrica está produzindo oMessage
, como você já está zombando disso. Não estamos testando oMessage
, como deveria ser testado em outros lugares, presumivelmente em um conjunto de testes para a API de terceiros que gera oMessage
. Na segunda linha, podemos ver pela inspeção que o método é simplesmente chamado noMessage
e, portanto, não há realmente nada para testar na segunda linha. Mais uma vez, deve haver testes em outros lugares que tornem esse teste redundante. A terceira linha chama aMessageLayer
API e simplesmente passa pelo resultado. De novo,MessageLayer
A API já deve ser testada em outro lugar. Isso nos deixa essencialmente com nada a testar. Não há efeitos colaterais visíveis diretos no código externo e não devemos testar a implementação interna. Isso nos leva à conclusão de que seria inapropriado testar esse código . (Para saber mais sobre esse raciocínio, consulte a apresentação de Sandi Metz, Truques mágicos de teste , [ slides , vídeo ])Pergunta 2: Espere, então ... o que? Sim, está certo, não teste isso. Agora, como mencionado, esta é uma versão abreviada do código. Se você tiver outra lógica, teste-a, mas encapsule-a em uma unidade separada (como a
MessageSender
implementação mencionada acima). Você pode simular todo esse aspecto do código facilmente, enquanto ainda tem a capacidade de testar outra lógica.Você está basicamente usando uma API de terceiros diretamente no seu código. O código de terceiros é notoriamente difícil de testar porque pode ter esses tipos de problemas de dependência que você tem aqui. Encapsulá-lo em uma área encurralada pode facilitar o teste de outro código e reduzir a cirurgia de espingarda se esse terceiro alterar seu código (ou apenas mudar). Embora ainda exista dificuldade em testar a parte que interage com a API de terceiros, ela está limitada a uma pequena faceta que você pode isolar.
fonte
Message
deve ser usado.Message
implementação real depende da API de terceiros; para poder criar instâncias de suas classes, você precisa iniciá-las, oEngine
que exige, entre outras coisas, uma chave de licença ... não exatamente o que você deseja em um teste de unidade.Vou concordar com @Robert Harvey. Só para esclarecer: Demeter é um estilo de programação razoável e bom. É o "sem zombarias de zombarias" que me parece mais uma preferência subjetiva que apóia um estilo de codificação específico, em vez de uma prática geralmente aplicável (e bem justificada). A regra das fadas utiliza interfaces "fluentes" como:
É um exemplo extremo, mas essencialmente a regra das fadas não permitiria incluir essa classe em qualquer código, porque o código se tornaria não testável. Mas esse é um paradigma popular no código OO.
Além disso, o problema mais geral é como zombar de uma fábrica para retornar uma zombaria com a qual você deseja testar. Geralmente, tenho vergonha de usar Fábricas como dependências, mas às vezes é muito melhor que a alternativa. Se você acabar com
Não vejo como você pode contornar isso. Você precisa de um mock para retornar o mock. Então, essa regra meio que derruba dois padrões de design OO realmente poderosos.
Não consigo pensar em uma maneira de solucionar seu problema sem dividir esse método em 2 ou 3, forçando longas filas até o cliente ou criando uma classe de invólucro com estado estranha.
Eu ficaria realmente interessado em ver como é a alternativa.
Minha resposta: seu código está bom! Excelcior!
(na verdade, estou curioso sobre alternativas)
...
Vamos considerar um caso de fronteira: as zombarias podem retornar por si mesmas? Tecnicamente, eles estão retornando uma farsa. Caso contrário, isso derruba o protótipo do GoF e esse é um dos padrões que se mantém ao longo do tempo:
a regra também permite:
Além disso, a regra das fadas praticamente proíbe o uso de mônadas, porque elas são baseadas no encadeamento de operações para um tipo de contêiner específico. Você pode testar o tipo de Mônada, mas não pode testar o código no qual a Mônada aparece.
fonte
obj.law().of().demeter();
é o mesmo queobj.law(); obj.of(); obj.demeter();
, o que é perfeitamente aceitável. Uma explicação semelhante pode ser feita sobre protótipos.