TDD: zombando de objetos fortemente acoplados

10

Às vezes, os objetos só precisam ser bem acoplados. Por exemplo, uma CsvFileclasse provavelmente precisará trabalhar estreitamente com a CsvRecordclasse (ou ICsvRecordinterface).

No entanto, pelo que aprendi no passado, um dos principais princípios do desenvolvimento orientado a testes é "Nunca teste mais de uma classe de cada vez". Significando que você deve usar ICsvRecordzombarias ou stubs em vez de instâncias reais de CsvRecord.

No entanto, depois de tentar essa abordagem, notei que zombar da CsvRecordclasse pode ficar um pouco peludo. O que me leva a uma das duas conclusões:

  1. É difícil escrever testes de unidade! Isso é um cheiro de código! Refatorar!
  2. Zombar de todas as dependências é irracional.

Quando substituí minhas zombarias por CsvRecordinstâncias reais , as coisas foram muito mais tranqüilas. Ao procurar os pensamentos de outras pessoas, me deparei com este post do blog , que parece apoiar o item 2 acima. Para objetos naturalmente acoplados, não devemos nos preocupar tanto com zombaria.

Estou fora dos trilhos? Existem desvantagens na suposição nº 2 acima? Eu deveria realmente estar pensando em refatorar meu design?

Phil
fonte
11
Eu acho que é um equívoco comum que a "unidade" em "testes de unidade" deva necessariamente ser uma classe. Acho que seu exemplo mostra um caso em que pode ser melhor que essas duas classes formem uma unidade. Mas não me entenda mal, concordo totalmente com a resposta de Robert Harvey.
Doc Brown

Respostas:

11

Se você realmente precisa de coordenação entre essas duas classes, escreva uma CsvCoordinatorclasse que encapsule suas duas classes e teste-a.

No entanto, discuto a noção que CsvRecordnão é testável de forma independente. CsvRecordé basicamente uma classe de DTO , não é? É apenas uma coleção de campos, talvez com alguns métodos auxiliares. E CsvRecordpode ser usado em outros contextos além CsvFile; você pode ter uma coleção ou matriz de CsvRecords, por exemplo.

Teste CsvRecordprimeiro. Certifique-se de que ele passe em todos os seus testes. Em seguida, vá em frente e use CsvRecordsua CsvFileclasse durante o teste. Use-o como um esboço / simulação pré-testado; preencha-o com dados de teste relevantes, passe-o para CsvFilee escreva seus casos de teste contra isso.

Robert Harvey
fonte
11
Sim, o CsvRecord é definitivamente mais testável de forma independente. O problema é que, se algo quebrar no CsvRecord, fará com que os testes do CsvData falhem. Mas não acho que isso seja uma questão importante.
Phil
11
Eu acho que você quer que isso aconteça. :)
Robert Harvey
11
@RobertHarvey: em teoria, pode se tornar um problema se CsvRecord e CsvFile estiverem se tornando classes bastante complexas e se um teste for interrompido para CsvFile, agora você não sabe imediatamente se é um problema no CsvFile ou no CsvRecord. Mas acho que esse é um caso mais hipotético - se eu tivesse a tarefa de programar essas classes para um programa do mundo real, faria exatamente da maneira que você descreve.
Doc Brown
2
@ Phil: Se CsvRecordquebra, então obviamente CsvDatafalha; mas tudo bem, porque você faz o teste CsvRecordprimeiro e, se isso falhar, seus CsvFiletestes não terão sentido. Você ainda pode distinguir entre erros dentro CsvRecorde dentro CsvFile.
tdammers
5

O motivo para testar uma classe de cada vez é que você não deseja que os testes de uma classe tenham dependências no comportamento de uma segunda classe. Isso significa que, se o seu teste para a Classe A exercer alguma das funcionalidades da Classe B, você deverá zombar da Classe B para remover a dependência de determinadas funcionalidades da Classe B.

Uma classe como CsvRecordme parece ser principalmente para armazenamento de dados - não é uma classe com muita funcionalidade própria. Ou seja, pode ter construtores, getters, setters, mas nenhum método com lógica substancial real. Claro, acho que sim - talvez você tenha escrito uma classe chamada CsvRecordque faz vários cálculos complexos.

Mas se CsvRecordnão tiver uma lógica real própria, não há nada a ganhar com zombaria dela. Esta é realmente apenas a máxima antiga - "não zombe de objetos de valor" .

Portanto, ao considerar a possibilidade de zombar de uma classe específica (para um teste de uma classe diferente), você deve levar em conta quanto de sua própria lógica essa classe possui e quanto dessa lógica será executada no decorrer de seu teste.

Dawood ibn Kareem
fonte
+1. Qualquer teste cujo resultado depende da correção do comportamento de mais de um objeto é um teste de integração, não um teste de unidade. Você precisa zombar de um desses objetos para obter um teste de unidade real. Isso não se aplica a objetos sem comportamento real neles - apenas com getters e setters, por exemplo.
precisa saber é o seguinte
1

No. 2 está bem. As coisas podem ser e devem ser fortemente acopladas se seus conceitos forem fortemente acoplados. Isso deve ser raro e geralmente evitado, mas no exemplo que você forneceu, faz sentido.

Telastyn
fonte
0

As classes "acopladas" são mutuamente dependentes uma da outra. Esse não deve ser o caso no que você está descrevendo - um CsvRecord realmente não deve se importar com o CsvFile que o contém, portanto, a dependência segue apenas um caminho. Isso é bom e não é um acoplamento apertado.

Afinal, se uma classe contiver a variável String name, você não afirmaria que está fortemente associado à String, não é?

Portanto, teste de unidade o CsvRecord para o comportamento desejado.

Em seguida, use uma estrutura de simulação (o Mockito é ótimo) para testar se sua unidade está interagindo com os objetos dos quais depende corretamente. O comportamento que você deseja testar, realmente - é que o CsvFile manipula CsvRcords da maneira esperada. O funcionamento interno do CvsRecord não deve importar - é como o CvsFile se comunica com ele.

Finalmente, o TDD não é apenas sobre testes de unidade. Você certamente pode (e deve) começar com testes funcionais que analisam o comportamento funcional de como seus componentes maiores funcionam - ou seja, sua história ou cenário do usuário. Os testes de suas unidades definem as expectativas e verificam as peças; os testes funcionais fazem o mesmo para o todo.

Matthew Flynn
fonte
11
-1, acoplamento rígido não significa necessariamente dependências cíclicas, isso é um equívoco. No exemplo, CsvFile está fortemente acoplado a CsvRecord(mas não o contrário). O OP pergunta se é uma boa idéia testar testando-o CsvFiledissociando-o de CsvRecordum ICsvRecord, e não vice-versa.
Doc Brown
2
@DocBrown: se o acoplamento é apertado ou não, CsvFiledepende de quanto depende do funcionamento interno CsvRecord, ou seja, da quantidade de suposições que o arquivo tem sobre o registro. As interfaces ajudam a documentar e aplicar essas suposições (ou melhor, a ausência de outras suposições), mas a quantidade de acoplamento permanece a mesma, exceto que, com uma interface, você pode conectar uma classe de registro diferente CsvFile. A introdução da interface apenas para você dizer que reduziu o acoplamento é bobagem.
tdammers
0

Há realmente duas perguntas aqui. A primeira é se existem situações em que zombar de um objeto é desaconselhável. Isso é indubitavelmente verdade, como mostra as outras excelentes respostas. A segunda pergunta é se o seu caso específico é uma dessas situações. Sobre essa questão, não estou convencido.

Provavelmente, o motivo mais comum para não zombar de uma classe é se é uma classe de valor. No entanto, você deve examinar o motivo por trás da regra. Não é porque a classe ridicularizada seja ruim de alguma forma, é porque será essencialmente idêntica à original. Se fosse esse o caso, seu teste de unidade não seria mais fácil usando a classe original.

Pode muito bem ser que seu código seja uma das raras exceções em que a refatoração não ajudaria, mas você deve declara-lo apenas depois que esforços diligentes de refatoração não funcionarem. Até desenvolvedores experientes podem ter problemas para encontrar alternativas ao seu próprio design. Se você não conseguir pensar em uma maneira possível de aprimorá-lo, peça a alguém experiente para dar uma segunda olhada.

A maioria das pessoas parece estar assumindo que você CsvRecordé uma classe de valor. Tente fazer um. Torne imutável, se puder. Se você tiver dois objetos com ponteiros um para o outro, remova um deles e descubra como fazê-lo funcionar. Procure lugares para dividir classes e funções. O melhor local para dividir uma classe nem sempre pode corresponder ao layout físico do arquivo. Tente reverter o relacionamento pai / filho das classes. Talvez você precise de uma classe separada para ler e gravar arquivos csv. Talvez você precise de classes separadas para manipular a E / S do arquivo e a interface para as camadas superiores. Há muitas coisas para tentar antes de declarar isso não-refatorável.

Karl Bielefeldt
fonte