Como os erros de digitação seriam detectados ao criar zombarias em um idioma dinâmico?

10

O problema ocorre ao fazer o TDD. Após algumas passagens no teste, os tipos de retorno de alguma classe / módulo são alterados. Em uma linguagem de programação estaticamente tipada, se um objeto simulado anterior foi usado nos testes de alguma outra classe e não foi modificado para refletir a alteração de tipo, ocorrerão erros de compilação.

No entanto, para idiomas dinâmicos, a alteração nos tipos de retorno pode não ser detectada e os testes da outra classe ainda serão aprovados. Certamente, pode haver testes de integração que falharão mais tarde, mas os testes de unidade serão erroneamente aprovados. Existe alguma maneira de evitar isso?

Atualizando com uma amostra trivial (em alguma linguagem inventada) ...

Versão 1:

Calc = {
    doMultiply(x, y) {return x * y}
}
//.... more code ....

// On some faraway remote code on a different file
Rect = {
    computeArea(l, w) {return Calc.doMultipy(x*y)}
}

// test for Rect
testComputeArea() { 
    Calc = new Mock()
    Calc.expect(doMultiply, 2, 30) // where 2 is the arity
    assertEqual(30, computeArea)
}

Agora, na versão 2:

// I change the return types. I also update the tests for Calc
Calc = {
    doMultiply(x, y) {return {result: (x * y), success:true}}
}

... O Rect lançará uma exceção no tempo de execução, mas o teste ainda terá êxito.

jvliwanag
fonte
11
O que as respostas até agora parecem faltar é que a pergunta não é sobre os testes que envolvem as alterações class X, mas class Ycujos testes dependem Xe, portanto, são testados contra um contrato diferente do que é executado na produção.
Bart van Ingen Schenau
Acabei de fazer esta pergunta no SO , em relação à injeção de dependência. Consulte Razão 1: Uma classe dependente pode ser alterada em tempo de execução (pense em teste) . Nós dois temos a mesma mentalidade, mas faltam grandes explicações.
Scott Coates
Estou relendo sua pergunta, mas estou ficando um pouco confuso com a interpretação. você pode dar um exemplo?
Scott Coates

Respostas:

2

Até certo ponto, isso é apenas parte do custo de se fazer negócios com linguagens dinâmicas. Você tem muita flexibilidade, também conhecida como "corda suficiente para se enforcar". Tenha cuidado com isso.

Para mim, o problema sugere o uso de técnicas de refatoração diferentes das usadas em uma linguagem de tipo estaticamente. Em uma linguagem estática, você substitui os tipos de retorno em parte para poder "confiar no compilador" para descobrir quais lugares podem ser quebrados. É seguro e provavelmente mais seguro do que modificar o tipo de retorno no local.

Em uma linguagem dinâmica, você não pode fazer isso; portanto, é muito mais seguro modificar o tipo de retorno no lugar, em vez de substituí-lo. Possivelmente, você a modifica adicionando sua nova classe a ela como membro, para as classes que precisam.

Tallseth
fonte
2

Se o seu código for alterado e os testes continuarem aprovados, haverá algo errado com os testes (ou seja, você está perdendo uma afirmação) ou o código não foi realmente alterado.

O que quero dizer com isso? Bem, seus testes descrevem os contratos entre partes do seu código. Se o contrato for "o valor de retorno deve ser iterável", a alteração do valor de retorno de, digamos, uma matriz para uma lista não é realmente uma alteração no contrato e, portanto, não provocará necessariamente uma falha no teste.

A fim de evitar afirmações falta, você pode usar ferramentas como análise de cobertura de código (não irá dizer-lhe que partes do seu código são testados, mas irá dizer-lhe que partes definitivamente não são testados), a complexidade ciclomática e complexidade nPath (que fornece um limite mais baixo ao número de asserções necessárias para obter uma cobertura completa dos códigos C1 e C2) e testadores de mutação (que injetam mutações no seu código, como transformar trueem falsenúmeros negativos em positivos, objetos em nulletc.) e verificar se isso faz com que os testes falhem).

Jörg W Mittag
fonte
+1 Algo errado nos testes ou no código não foi alterado. Demorei um tempo para me acostumar depois de anos de C ++ / Java. O contrato entre partes em linguagens dinâmicas de código não deve ser o que é retornado, mas o que essa coisa retornada contém e o que ela pode fazer.
MrFox
@suslik: Na verdade, não se trata de linguagens estáticas vs. dinâmicas, mas de abstração de dados usando tipos de dados abstratos versus abstração de dados orientada a objetos. A capacidade de um objeto simular outro, desde que se comporte indistinguível (mesmo que sejam de tipos completamente diferentes e instâncias de classes completamente diferentes) é fundamental para toda a ideia de OO. Ou, colocando de outra forma: se dois objetos se comportam da mesma forma, são do mesmo tipo, independentemente do que dizem as classes. Dito de outra maneira: classes não são tipos. Infelizmente, Java e C # estão errados.
Jörg W Mittag
Supondo que a unidade ridicularizada seja 100% coberta e que não exista nenhuma afirmação: que tal uma mudança mais sutil como "deve retornar nulo para foo" para "deve retornar string vazia para foo". Se alguém mudar essa unidade e seu teste, algumas unidades consumidoras poderão quebrar e, devido à simulação, isso é transparente. (E não: no momento da escrita ou uso do módulo zombado, por "contrato", não é necessário manipular cadeias vazias como valor de retorno, pois deve retornar cadeias não vazias ou somente nulas;)). (Apenas integração adequada testando de ambos os módulos na interação poderia pegar isso.)
try-catch-finally