Lutando com dependências cíclicas em testes de unidade


24

Estou tentando praticar o TDD, usando-o para desenvolver um simples como o Bit Vector. Por acaso, estou usando o Swift, mas essa é uma pergunta independente da linguagem.

My BitVectoré um structque armazena um único UInt64e apresenta uma API sobre ele que permite tratá-lo como uma coleção. Os detalhes não importam muito, mas são bem simples. Os 57 bits altos são bits de armazenamento e os 6 bits inferiores são bits de "contagem", o que indica quantos bits de armazenamento realmente armazenam um valor contido.

Até agora, tenho um punhado de recursos muito simples:

  1. Um inicializador que constrói vetores de bits vazios
  2. Uma countpropriedade do tipoInt
  3. Uma isEmptypropriedade do tipoBool
  4. Um operador de igualdade ( ==). Nota: este é um operador de igualdade de valor semelhante ao Object.equals()Java, não um operador de igualdade de referência como ==no Java.

Estou enfrentando várias dependências cíclicas:

  1. O teste de unidade que testa meu inicializador precisa verificar se o recém-construído BitVector. Isso pode ser feito de três maneiras:

    1. Verifica bv.count == 0
    2. Verifica bv.isEmpty == true
    3. Verifique que bv == knownEmptyBitVector

    O método 1 depende count, o método 2 depende isEmpty(o qual depende count, então não faz sentido usá-lo), o método 3 depende ==. De qualquer forma, não consigo testar meu inicializador isoladamente.

  2. O teste countprecisa operar com algo que inevitavelmente testa meus inicializadores

  3. A implementação de se isEmptybaseia emcount

  4. A implementação de ==depende count.

Consegui resolver parcialmente esse problema, introduzindo uma API privada que constrói a BitVectorpartir de um padrão de bits existente (como a UInt64). Isso me permitiu inicializar valores sem testar nenhum outro inicializador, para que eu pudesse "arrancar a correia" no meu caminho.

Para que meus testes de unidade sejam realmente testes de unidade, eu me vejo fazendo um monte de hacks, o que complica substancialmente meu código de teste e teste.

Como exatamente você contorna esses tipos de problemas?


20
Você está adotando uma visão muito restrita do termo "unidade". BitVectoré um tamanho de unidade perfeitamente adequado para testes de unidade e resolve imediatamente seus problemas de que os membros públicos BitVectorprecisam um do outro para fazer testes significativos.
Bart van Ingen Schenau

Você conhece muitos detalhes de implementação antecipadamente. Seu desenvolvimento é realmente orientado a testes ?
herby

@herby Não, é por isso que estou praticando. Embora isso pareça um padrão realmente inatingível. Não acho que tenha programado nada sem uma aproximação mental bastante clara do que a implementação implicará.
Alexander - Restabelecer Monica

@ Alexander Você deve tentar relaxar isso, caso contrário, será o primeiro teste, mas não o teste. Apenas diga vagamente: "Farei um vetor de bits com um int de 64 bits como loja de apoio" e é isso; a partir desse ponto, refaça TDD vermelho-verde-um após o outro. Os detalhes da implementação, bem como a API, devem surgir da tentativa de executar os testes (o primeiro) e da gravação desses testes em primeiro lugar (o último).
herby

Respostas:


66

Você está preocupado demais com os detalhes da implementação.

Não importa que, na sua implementação atual , isEmptydependa count(ou de qualquer outro relacionamento que você possa ter): tudo com o que você deve se preocupar é com a interface pública. Por exemplo, você pode ter três testes:

  • Que um objeto recém-inicializado possui count == 0.
  • Que um objeto recém-inicializado tenha isEmpty == true
  • Que um objeto recém-inicializado seja igual ao objeto vazio conhecido.

Todos esses são testes válidos e se tornam especialmente importantes se você decidir refatorar os internos de sua classe para que isEmptytenha uma implementação diferente da qual não depende count- desde que todos os seus testes ainda passem, você sabe que não regrediu qualquer coisa.

Coisas semelhantes se aplicam a seus outros pontos - lembre-se de testar a interface pública, não sua implementação interna. Você pode achar o TDD útil aqui, pois você escreveria os testes necessários isEmptyantes de escrever qualquer implementação para ele.


6
@ Alexander Você parece um homem que precisa de uma definição clara de teste de unidade. O melhor que eu sei vem de Michael Feathers
candied_orange

14
@ Alexander, você está tratando cada método como um trecho de código testável independentemente. Essa é a fonte de suas dificuldades. Essas dificuldades desaparecem se você testar o objeto como um todo, sem tentar dividi-lo em partes menores. Dependências entre objetos não são comparáveis ​​com dependências entre métodos.
amon

9
@ Alexander "um pedaço de código" é uma medida arbitrária. Apenas inicializando uma variável, você está usando muitos "pedaços de código". O que importa é que você está testando uma unidade comportamental coesa, conforme definido por você .
Ant P

9
"Pelo que li, tive a impressão de que, se você quebrar apenas um pedaço de código, apenas os testes de unidade diretamente relacionados a esse código deverão falhar". Essa parece ser uma regra muito difícil de seguir. (por exemplo, se você escrever uma classe de vetor e cometer um erro no método index, provavelmente terá toneladas de quebra em todo o código que usa essa classe de vetor)
jhominal

4
@ Alexander Além disso, observe o padrão "Organizar, agir, afirmar" para testes. Basicamente, você configura o objeto em qualquer estado em que ele esteja (Organizar), chame o método que está realmente testando (Act) e verifique se o estado dele mudou de acordo com suas expectativas. (Afirmar). O material que você configurou no Arrange seria "pré-condição" para o teste.
GalacticCowboy

5

Como exatamente você contorna esses tipos de problemas?

Você revisa seu pensamento sobre o que é um "teste de unidade".

Um objeto que gerencia dados mutáveis ​​na memória é fundamentalmente uma máquina de estado. Portanto, qualquer caso de uso valioso invoca, no mínimo, um método para colocar informações no objeto e invoca um método para ler uma cópia das informações do objeto. Nos casos de uso interessantes, você também invocará métodos adicionais que alteram a estrutura de dados.

Na prática, isso geralmente parece

// GIVEN
obj = new Object(...)

// THEN
assert object.read(...)

ou

// GIVEN
obj = new Object(...)

// WHEN
object.change(...)

// THEN
assert object.read(...)

A terminologia do "teste de unidade" - bem, ele tem uma longa história de não ser muito bom.

Eu os chamo de testes de unidade, mas eles não correspondem muito bem à definição aceita de testes de unidade - Kent Beck, Desenvolvimento Orientado a Testes por Exemplo

Kent escreveu a primeira versão do SUnit em 1994 , o porto para JUnit foi em 1998, o primeiro rascunho do livro TDD foi no início de 2002. A confusão demorou muito tempo para se espalhar.

A idéia principal desses testes (mais precisamente chamados de "testes de programador" ou "testes de desenvolvedor") é que os testes são isolados um do outro. Os testes não compartilham nenhuma estrutura de dados mutáveis, para que possam ser executados simultaneamente. Não há preocupações de que os testes sejam executados em uma ordem específica para medir corretamente a solução.

O principal caso de uso desses testes é que eles são executados pelo programador entre as edições em seu próprio código-fonte. Se você estiver executando o protocolo de refatoração verde vermelho, um vermelho inesperado sempre indica uma falha em sua última edição; você reverte essa alteração, verifique se os testes são VERDES e tente novamente. Não há muita vantagem em tentar investir em um design em que todo e qualquer bug possível seja capturado por apenas um teste.

Obviamente, uma mesclagem introduz uma falha e, em seguida, descobrir que a falha não é mais trivial. Existem várias etapas que você pode executar para garantir que as falhas sejam fáceis de localizar. Vejo


1

Em geral (mesmo que não esteja usando o TDD), você deve se esforçar para escrever os testes o máximo possível, fingindo não saber como é implementado.

Se você está realmente fazendo TDD, esse já deve ser o caso. Seus testes são uma especificação executável do programa.

A aparência do gráfico de chamada abaixo dos testes é irrelevante, desde que os próprios testes sejam sensatos e bem mantidos.

Acho que seu problema é sua compreensão do TDD.

Seu problema, na minha opinião, é que você está "misturando" suas personas de TDD. Suas personas "teste", "código" e "refatorar" operam de forma completamente independente uma da outra, idealmente. Em particular, suas personas de codificação e refatoração não têm obrigações para com os testes, exceto para fazê-las / mantê-las funcionando em verde.

Claro, em princípio, seria melhor se todos os testes fossem ortogonais e independentes um do outro. Mas isso não é uma preocupação de suas outras duas pessoas TDD, e definitivamente não é um requisito rígido estrito ou mesmo necessariamente realista de seus testes. Basicamente: não descarte seus sentimentos de senso comum sobre a qualidade do código para tentar atender a um requisito que ninguém está pedindo a você.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.