Teste de unidade, fábricas e a Lei de Demeter

8

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 orderIDo loginIDque não serão alteradas durante a vida útil do aplicativo.

Então, quando eu crio myOrderoriginalmente, injeto a MessageFactory, o que sabe loginID. Então, quando updateQuantityé chamado, o Orderpassa adiante orderID. O código de controle é fácil de escrever. Outro encadeamento lida com o retorno de chamada e as atualizações, Orderse a alteração foi bem-sucedida, ou informa Orderque sua alteração falhou, se não foi.

O problema está testando. Como o Orderobjeto depende de a MessageFactorye precisa MessageFactoryretornar Messages reais (que são chamados .setOrderID(), por exemplo), agora tenho que configurar MessageFactoryzombarias 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:

  1. 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?
  2. Crie uma implementação somente de teste MessageFactorye 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);
    }
}
durron597
fonte
Eu acho que você terá que nos mostrar o código relevante que você escreveu para o Orderobjeto e o MessageFactory. Esta é uma boa descrição, mas é um pouco abstrato abordar diretamente com uma resposta clara.
22714 Robert
@RobertHarvey Espero que a atualização ajude.
durron597
Você está tentando verificar se layer.senda mensagem é enviada ou se ela está correta ?
Robert Harvey
@RobertHarvey Eu estava pensando em chamar verify(messageMock).setQuantity(2), e verify(layer).send(messageMock);também o updateQuantitydeve retornar false se o Orderjá tiver uma atualização pendente, mas eu omiti esse código por razões de sscce.
durron597
3
Vou ter que olhar um pouco mais para o seu código ... Mas, para constar, não considero que devolver uma farsa seja tão importante. A complexidade do teste baseado em simulação é uma consequência inevitável de lidar com objetos mutáveis ​​e, como uma simulação não tem influência no seu aplicativo final lançado, não vejo como retornar alguém mata gatinhos, muito menos fadas.
Robert Harvey

Respostas:

12

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 a Messageaula 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 Messagevocê pode retornar? Bem, você pode retornar um real completo Message, um real simplificado Message(em que são usados ​​padrões conhecidos) ou pode retornar um NullMessage(como no Padrão de Objeto Nulo). A NullMessageé tão válido Messagequanto 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 OrderBuilderdeve criar esses objetos e passá-los como seus próprios parâmetros.

Para testar isso, você passaria um mock MessageFactory, que retornaria um real Message(completo, simples ou nulo) e um mock MessageLayerque recebesse a mensagem. Se você usar um completo ou simplificado Message, poderá recuperá-lo do seu MessageLayermock e inspecioná-lo para testar asserções.

Também gostaria de olhar para o MessageFactorye MessageLayercomo uma moita funcionalidade em um nível diferente de abstração, e por isso gostaria de extrair uma MessageSenderclasse que encapsulado essa funcionalidade. Você pode testar esta classe usando uma simulação simples MessageSendere mudar tudo o que eu falei acima para os MessageSendertestes, 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 o Message, como você já está zombando disso. Não estamos testando o Message, como deveria ser testado em outros lugares, presumivelmente em um conjunto de testes para a API de terceiros que gera o Message. Na segunda linha, podemos ver pela inspeção que o método é simplesmente chamado no Messagee, 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 a MessageLayerAPI e simplesmente passa pelo resultado. De novo,MessageLayerA 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 MessageSenderimplementaçã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.

cbojar
fonte
+1 Concordo totalmente com o construtor. Não tenho certeza se o retorno do NullMessage é melhor do que o retorno de um Mock - parece uma falha técnica. De qualquer forma, o resultado é "falso".
21414 Rob Rob
Lol Eu roubei o truque "pegar o próprio construtor como argumento" da fonte do Guava.
durron597
@RobY Um objeto nulo não precisa ser falso, se você o usar como parte de seu aplicativo como uma operação legítima. Porém, se ele realmente é usado apenas como um teste falso, outro tipo de real Messagedeve ser usado.
Cbojar
@cbojar Concedido, mas Opcional / Talvez esteja surgindo como uma maneira melhor de lidar com referências anuláveis. Se você está escrevendo apenas um NullObject para testes de unidade, o NullObject é realmente apenas uma farsa escrita à mão, como nos velhos tempos. O problema é que, sem uma justificativa clara para a regra "não zomba dos zombes", é difícil dizer se um NullObject é ruim ou por quê , exceto que ele pode ou não violar uma regra estilística subjetiva.
Rob
BTW, a única Messageimplementação real depende da API de terceiros; para poder criar instâncias de suas classes, você precisa iniciá-las, o Engineque exige, entre outras coisas, uma chave de licença ... não exatamente o que você deseja em um teste de unidade.
durron597
2

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:

Thing.create ("zoom"). SetDomain ("bo.com"). Add (1) .flip (). Reverse (). TuneForSemantics (). Run ();

É 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

ThirdPartyThing ThirdPartyFactory<ThirdPartyThing>#create()

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:

MuActor prototype = ...
...
MuActor actor = prototype.create();
actor.run();

a regra também permite:

prototype = Mock(MuActor.class);
when(prototype.create()).thenReturn(prototype);

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.

Roubar
fonte
As interfaces fluentes ainda podem seguir Demeter porque as interfaces fluidas retornam o mesmo objeto. Em outras palavras, obj.law().of().demeter();é o mesmo que obj.law(); obj.of(); obj.demeter();, o que é perfeitamente aceitável. Uma explicação semelhante pode ser feita sobre protótipos.
Cbojar
Sim, mas se eles realmente funcionarem e você quiser zombar deles, você se deparará com a regra "não zomba dos zombadores". O que significa que o código que os utiliza se torna não testável.
22414 Rob Rob
@ cbojar oh, e eu devo ser claro. Demeter parece razoável, e um bom estilo de programação. É o "sem zombarias dos zombes" que me parece mais uma preferência subjetiva que apóia um estilo de codificação específico do que uma prática geralmente aplicável (e bem justificada).
Rob
A propósito, em uma parte diferente do código eu uso a interface fluente. Eu tenho algumas idéias deste post do blog , e aqui está o código real que eu uso: pastebin.com/D1GxSPsy
durron597
Isso é esperto. Legal! Vou tentar no código em que estou trabalhando agora. Apenas uma nota - uma interface fluente não precisa retornar por si mesma. Existe a variação em que a interface fluente retorna uma série de instâncias imutáveis. ou seja, em vez de "retornar isso", ele usa "retornar novo Fluente (...)". No entanto, isso é transparente para o cliente, portanto não afeta nenhuma das discussões aqui. Mas é um fato divertido.
Rob