O código testável é melhor?

100

Estou tentando adquirir o hábito de escrever testes de unidade regularmente com meu código, mas li que primeiro é importante escrever código testável . Esta questão toca nos princípios SOLID de escrever código testável, mas eu quero saber se esses princípios de design são benéficos (ou pelo menos não prejudiciais) sem planejar escrever testes. Para esclarecer - eu entendo a importância de escrever testes; isso não é uma questão sobre sua utilidade.

Para ilustrar minha confusão, em a peça que inspirou esta questão, o escritor dá um exemplo de uma função que verifica a hora atual e retorna algum valor dependendo da hora. O autor aponta para isso como código ruim porque produz os dados (o tempo) que usa internamente, dificultando assim o teste. Para mim, no entanto, parece um exagero passar no tempo como um argumento. Em algum momento, o valor precisa ser inicializado e por que não mais próximo do consumo? Além disso, o propósito do método em minha mente é retornar algum valor com base no tempo atual , tornando-o um parâmetro que você insinua que essa finalidade pode / deve ser alterada. Isso e outras questões me levam a pensar se o código testável era sinônimo de código "melhor".

Está escrevendo código testável ainda boas práticas mesmo na ausência de testes?

O código testável é realmente mais estável? foi sugerido como uma duplicada. No entanto, essa pergunta é sobre a "estabilidade" do código, mas estou perguntando de maneira mais ampla se o código é superior também por outros motivos, como legibilidade, desempenho, acoplamento e assim por diante.

    
por WannabeCoder 01.07.2015 / 16:59
fonte

12 respostas

112

No que diz respeito à definição comum de testes unitários, eu diria que não. Eu vi um código simples confuso devido à necessidade de torçá-lo para se adequar à estrutura de teste (por exemplo, interfaces e IoC em todos os lugares, tornando as coisas difíceis de seguir através de camadas de chamadas de interface e dados que devem ser óbvios transmitidos por magia). Dada a escolha entre o código que é fácil de entender ou o código que é fácil de testar na unidade, eu sempre uso o código de manutenção.

Isso não significa não testar, mas encaixar as ferramentas de acordo com você, e não o contrário. Existem outras maneiras de testar (mas código difícil de entender é sempre código incorreto). Por exemplo, você pode criar testes unitários menos granulares (por exemplo, a atitude de Martin Fowler de que uma unidade é geralmente classe, não um método), ou você pode acessar seu programa com testes de integração automatizados. Tal pode não ser tão bonito como o seu quadro de testes se acende com carrapatos verdes, mas estamos atrás do código testado, não da gamificação do processo, certo?

Você pode tornar seu código fácil de manter e ainda ser bom para testes de unidade, definindo boas interfaces entre eles e, em seguida, escrevendo testes que exercitam a interface pública do componente; ou você pode obter uma estrutura de teste melhor (uma que substitua as funções em tempo de execução para ridicularizá-las, em vez de exigir que o código seja compilado com mocks no lugar). Uma estrutura de teste de unidade melhor permite que você substitua a funcionalidade GetCurrentTime () do sistema com a sua própria, em tempo de execução, para que você não precise introduzir wrappers artificiais para isso apenas para se adequar à ferramenta de teste.

    
por 01.07.2015 / 17:13
fonte
68

Is writing testable code still good practice even in the absence of tests?

Primeiramente, a ausência de testes é um problema muito maior do que seu código ser testável ou não. Não ter testes de unidade significa que você não terminou seu código / recurso.

Isso fora do caminho, eu não diria que é importante escrever código testável - é importante escrever código flexível . O código inflexível é difícil de testar, então há muita sobreposição na abordagem e no que as pessoas chamam.

Então, para mim, sempre há um conjunto de prioridades para escrever código:

  1. Faça funcionar - se o código não fizer o que precisa ser feito, não terá valor.
  2. Torne-o sustentável - se o código não puder ser mantido, ele irá rapidamente parar de funcionar.
  3. Torne-o flexível - se o código não for flexível, ele deixará de funcionar quando os negócios inevitavelmente chegarem e perguntar se o código pode fazer XYZ.
  4. Faça isso rápido - além de um nível aceitável, o desempenho é apenas molho.

Testes de unidade ajudam na manutenção do código, mas apenas até certo ponto. Se você tornar o código menos legível ou mais frágil para que os testes de unidade funcionem, isso será contraproducente. "Código testável" é geralmente um código flexível, o que é bom, mas não tão importante quanto funcionalidade ou capacidade de manutenção. Para algo como o tempo atual, tornar isso flexível é bom, mas prejudica a manutenção, tornando o código mais difícil de usar e mais complexo. Como a capacidade de manutenção é mais importante, normalmente irei errar em direção à abordagem mais simples, mesmo que seja menos testável.

    
por 01.07.2015 / 17:17
fonte
48

Sim, é uma boa prática. O motivo é que a testabilidade não é por causa de testes. É por uma questão de clareza e compreensão que isso traz consigo.

Ninguém se importa com os testes em si. É um triste fato da vida que precisamos de grandes suítes de teste de regressão, porque não somos brilhantes o suficiente para escrever código perfeito sem constantemente checar nossos passos. Se pudéssemos, o conceito de testes seria desconhecido, e tudo isso não seria um problema. Eu certamente gostaria de poder. Mas a experiência tem mostrado que quase todos nós não podemos, portanto, testes cobrindo nosso código são uma coisa boa, mesmo que eles tirem tempo de escrever códigos de negócios.

Como é que os testes melhoram o nosso código de negócios independentemente do teste em si? Forçando-nos a segmentar nossa funcionalidade em unidades que são facilmente demonstradas como corretas. Essas unidades também são mais fáceis de acertar do que as que, de outra forma, seríamos tentados a escrever.

Seu exemplo de tempo é um bom ponto. Contanto que você somente tenha uma função retornando a hora atual, você pode pensar que não faz sentido programá-la. Quão difícil pode ser para acertar isso? Mas, inevitavelmente, seu programa irá usar essa função em outro código, e você definitivamente desejará testar esse código sob diferentes condições, incluindo em momentos diferentes. Portanto, é uma boa ideia poder manipular o tempo que sua função retorna - não porque você desconfia de sua chamada de currentMillis() de uma linha, mas porque você precisa verificar os chamadores dessa chamada sob controle circunstâncias. Então, você vê, ter o código testável é útil mesmo se por si só, não parece merecer tanta atenção.

    
por 01.07.2015 / 17:09
fonte
12

At some point the value needs to be initialized, and why not closest to consumption?

Porque você pode precisar reutilizar esse código, com um valor diferente do gerado internamente. A capacidade de inserir o valor que você usará como parâmetro garante que você possa gerar esses valores com base na hora que desejar, não apenas "agora" (com "agora" significando quando você chama o código).

Tornar o código testável em vigor significa criar código que pode (desde o início) ser usado em dois cenários diferentes (produção e teste).

Basicamente, embora você possa argumentar que não há incentivo para tornar o código testável na ausência de testes, há grande vantagem em escrever códigos reutilizáveis, e os dois são sinônimos.

Plus, the purpose of the method in my mind is to return some value based on the current time, by making it a parameter you imply that this purpose can/should be changed.

Você também pode argumentar que o propósito desse método é retornar algum valor com base em um valor de tempo, e você precisa gerar esse valor com base em "now". Um deles é mais flexível e, se você se acostumar a escolher essa variante, com o tempo, sua taxa de reutilização de código aumentará.

    
por 01.07.2015 / 17:37
fonte
10

Pode parecer bobo dizer isso dessa maneira, mas se você quiser testar seu código, então sim, escrever código testável é melhor. Você pergunta:

At some point the value needs to be initialized, and why not closest to consumption?

Precisamente porque, no exemplo a que você está se referindo, isso torna esse código impossível de ser testado. A menos que você execute apenas um subconjunto de seus testes em diferentes momentos do dia. Ou você redefinir o relógio do sistema. Ou alguma outra solução alternativa. Tudo o que é pior do que simplesmente tornar seu código flexível.

Além de ser inflexível, esse pequeno método em questão tem duas responsabilidades: (1) obter o tempo do sistema e, em seguida, (2) retornar algum valor baseado nele.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

Faz sentido dividir ainda mais as responsabilidades, para que a parte fora do seu controle ( DateTime.Now ) tenha o menor impacto no restante do seu código. Fazer isso tornará o código acima mais simples, com o efeito colateral de ser sistematicamente testável.

    
por 01.07.2015 / 17:20
fonte
9

Certamente tem um custo, mas alguns desenvolvedores estão tão acostumados a pagar que esqueceram que o custo está lá. No seu exemplo, agora você tem duas unidades em vez de uma, você precisou do código de chamada para inicializar e gerenciar uma dependência adicional e, embora GetTimeOfDay seja mais testável, você está de volta ao mesmo barco testando seu novo IDateTimeProvider . É que, se você tiver bons testes, os benefícios geralmente superam os custos.

Além disso, até certo ponto, escrever código testável incentiva você a arquitetar seu código de maneira mais sustentável. O novo código de gerenciamento de dependência é chato, então você deve agrupar todas as funções dependentes de tempo, se possível. Isso pode ajudar a mitigar e corrigir erros como, por exemplo, quando você carrega uma página diretamente em um limite de tempo, tendo alguns elementos renderizados usando o tempo anterior e alguns usando o tempo posterior. Também pode acelerar o seu programa, evitando repetidas chamadas do sistema para obter a hora atual.

Naturalmente, essas melhorias na arquitetura são altamente dependentes de alguém perceber as oportunidades e implementá-las. Um dos maiores perigos de se concentrar tanto nas unidades é perder de vista o quadro maior.

Muitas estruturas de testes de unidade permitem que você conserte um objeto falso em tempo de execução, o que permite que você obtenha os benefícios da testabilidade sem toda a bagunça. Eu até vi isso feito em C ++. Olhe para essa habilidade em situações em que parece que o custo da testabilidade não vale a pena.

    
por 01.07.2015 / 17:53
fonte
8

É possível que nem todas as características que contribuem para a testabilidade sejam desejáveis fora do contexto de testabilidade - tenho dificuldade em apresentar uma justificação não relacionada ao teste para o parâmetro de tempo que você cita, por exemplo - mas falando amplamente as características que contribuir para a testabilidade também contribuem para um bom código, independentemente da testabilidade.

Em termos gerais, o código testável é um código maleável. É em pedaços pequenos, discretos e coesos, para que bits individuais possam ser chamados para reutilização. É bem organizado e bem conhecido (para poder testar algumas funcionalidades, você dá mais atenção à nomeação; se você não estivesse escrevendo, o nome de uma função de uso único importaria menos). Ele tende a ser mais paramétrico (como o seu exemplo de tempo), portanto, pode ser usado em outros contextos além do propósito original pretendido. É seco, menos confuso e mais fácil de compreender.

Sim. É uma boa prática escrever código testável, mesmo sem testes.

    
por 01.07.2015 / 17:26
fonte
8

Escrever código testável é importante se você quiser provar que seu código realmente funciona.

Eu tenho a tendência de concordar com os sentimentos negativos sobre distorcer seu código em contorções hediondas apenas para ajustá-lo a uma estrutura de teste específica.

Por outro lado, todos aqui, em algum momento, tiveram que lidar com aquela longa função mágica de 1.000 linhas que é apenas odiosa de se ter que lidar, virtualmente não pode ser tocada sem quebrar um ou mais obscuros , dependências não óbvias em algum outro lugar (ou em algum lugar dentro dele, onde a dependência é quase impossível de visualizar) e é praticamente por definição não testável. A noção (que não é sem mérito) de que os frameworks de testes se tornaram exagerados não deve ser tomada como uma licença livre para escrever código não testável e de baixa qualidade, na minha opinião.

Os ideais de desenvolvimento orientados por testes tendem a empurrá-lo para escrever procedimentos de responsabilidade única, por exemplo, e isso é definitivamente uma coisa boa. Pessoalmente, eu digo comprar em responsabilidade única, única fonte de verdade, escopo controlado (sem variáveis globais freakin) e manter dependências frágeis a um mínimo, e seu código será testável. Testável por algum framework de testes específico? Quem sabe. Mas talvez seja o framework de testes que precisa se ajustar a um bom código, e não o contrário.

Mas só para deixar claro, código tão inteligente, ou tão longo e / ou interdependente que não é facilmente compreendido por outro ser humano, não é um bom código. E também, coincidentemente, não é um código que possa ser facilmente testado.

Então, aproximando-se do meu resumo, o código testável é melhor ?

Eu não sei, talvez não. As pessoas aqui têm alguns pontos válidos.

Mas acredito que o código melhor tende a ser também um código testável .

E que se você estiver falando sobre software sério para uso em empreendimentos sérios, enviar códigos não testados não é a coisa mais responsável que você poderia estar fazendo com o dinheiro do seu empregador ou de seus clientes.

Também é verdade que algum código requer testes mais rigorosos do que outro código e é um pouco bobo fingir o contrário. Como você gostaria de ter sido um astronauta no ônibus espacial se o sistema de menus que o conectou com os sistemas vitais do ônibus espacial não foi testado? Ou um funcionário de uma usina nuclear onde os sistemas de software que monitoram a temperatura no reator não foram testados? Por outro lado, um pouco de código gerando um relatório de leitura simples requer um caminhão-contêiner cheio de documentação e mil testes de unidade? Eu espero que não. Apenas dizendo ...

    
por 02.07.2015 / 04:11
fonte
5

To me, though, it seems like overkill to pass in the time as an argument.

Você está certo, e com zombaria você pode tornar o código testável e evitar passar o tempo (intenção da punção indefinida). Exemplo de código:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Agora, digamos que você queira testar o que acontece durante um segundo. Como você disse, para testar essa maneira exagerada, você teria que alterar o código (de produção):

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Se o Python suportasse os segundos bissextos , o código de teste ficaria assim:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Você pode testar isso, mas o código é mais complexo do que o necessário e os testes ainda não podem executar com segurança a ramificação de código que a maioria dos códigos de produção usará (ou seja, não passar um valor para now ). Você contorna isso usando uma simulação . A partir do original código de produção:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Código de teste:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Isso traz vários benefícios:

  • Você está testando time_of_day independentemente de suas dependências.
  • Você está testando o mesmo caminho de código como código de produção.
  • O código de produção é o mais simples possível.

Em uma nota lateral, é de se esperar que futuras estruturas de zombaria tornem as coisas mais fáceis. Por exemplo, desde que você tenha que se referir à função zombada como uma string, você não pode facilmente fazer IDEs alterá-la automaticamente quando time_of_day começar a usar outra fonte de tempo.

    
por 04.07.2015 / 15:57
fonte
4

Uma qualidade de código bem escrito é que é robusto mudar . Ou seja, quando uma mudança de requisitos surge, a mudança no código deve ser proporcional. Este é um ideal (e nem sempre alcançado), mas escrever código testável ajuda a nos aproximar desse objetivo.

Por que isso nos ajuda a nos aproximar? Na produção, nosso código opera dentro do ambiente de produção, incluindo integração e interação com todos os outros códigos. Em testes de unidade, varramos muito desse ambiente. Nosso código agora está sendo robusto para mudar porque os testes são uma mudança . Estamos usando as unidades de maneiras diferentes, com diferentes entradas (mocks, entradas ruins que podem nunca ser transmitidas, etc.) do que as usamos na produção.

Isso prepara nosso código para o dia em que a mudança acontece em nosso sistema. Digamos que nosso cálculo de tempo precise de um horário diferente com base em um fuso horário. Agora temos a capacidade de passar esse tempo e não ter que fazer alterações no código. Quando não queremos passar de uma vez e queremos usar a hora atual, podemos usar apenas um argumento padrão. Nosso código é robusto para mudar porque é testável.

    
por 01.07.2015 / 17:34
fonte
4

De minha experiência, uma das decisões mais importantes e de maior alcance que você faz ao criar um programa é como você divide o código em unidades (onde "unidades" são usadas em sua forma mais ampla sentido). Se você estiver usando uma linguagem OO baseada em classe, precisará quebrar todos os mecanismos internos usados para implementar o programa em algum número de classes. Então você precisa quebrar o código de cada classe em um certo número de métodos. Em alguns idiomas, a escolha é como dividir seu código em funções. Ou se você fizer o SOA, você precisa decidir quantos serviços você irá construir e o que vai entrar em cada serviço.

A divisão que você escolhe tem um efeito enorme em todo o processo. Boas escolhas tornam o código mais fácil de escrever e resultam em menos erros (mesmo antes de você começar a testar e depurar). Eles facilitam a mudança e a manutenção. Curiosamente, uma vez que você encontra um bom colapso, geralmente também é mais fácil testar do que um pobre.

Por que isso é assim? Eu não acho que posso entender e explicar todas as razões. Mas uma razão é que um bom colapso invariavelmente significa escolher um "tamanho de grão" moderado para unidades de implementação. Você não quer empinar muita funcionalidade e muita lógica em uma única classe / método / função / módulo / etc. Isso torna seu código mais fácil de ler e mais fácil de escrever, mas também facilita o teste.

Não é só isso, no entanto. Um bom design interno significa que o comportamento esperado (entradas / saídas / etc) de cada unidade de implementação pode ser definido de forma clara e precisa. Isso é importante para testes. Um bom design geralmente significa que cada unidade de implementação terá um número moderado de dependências nas outras. Isso torna seu código mais fácil para os outros lerem e entenderem, mas também facilita o teste. As razões continuam; talvez outros possam articular mais razões que eu não posso.

Com relação ao exemplo em sua pergunta, não acho que "bom design de código" seja equivalente a dizer que todas as dependências externas (como uma dependência do relógio do sistema) devem sempre ser "injetadas". Isso pode ser uma boa ideia, mas é uma questão separada do que estou descrevendo aqui e não vou me aprofundar em seus prós e contras.

A propósito, mesmo se você fizer chamadas diretas para as funções do sistema que retornam a hora atual, agir no sistema de arquivos, e assim por diante, isso não significa que você não pode testar seu código separadamente. O truque é usar uma versão especial das bibliotecas padrão que permite falsificar os valores de retorno das funções do sistema. Eu nunca vi outros mencionarem essa técnica, mas é bastante simples de se fazer com muitas linguagens e plataformas de desenvolvimento. (Espero que seu tempo de execução de linguagem seja de código aberto e fácil de construir. Se a execução de seu código envolver uma etapa de link, esperamos que também seja fácil controlar com quais bibliotecas ele está vinculado.)

Em resumo, o código testável não é necessariamente um código "bom", mas o código "bom" geralmente é testável.

    
por 02.07.2015 / 08:28
fonte
1

Se você está seguindo os princípios SOLID , você estará bem, especialmente se estenda isto com KISS , DRY e YAGNI .

Um ponto em falta para mim é o ponto da complexidade de um método. É um método simples de getter / setter? Então, apenas escrever testes para satisfazer sua estrutura de testes seria uma perda de tempo.

Se é um método mais complexo, no qual você manipula dados e deseja ter certeza de que funcionará, mesmo que tenha que alterar a lógica interna, seria uma ótima opção para um método de teste. Muitas vezes eu tive que mudar um pedaço de código depois de vários dias / semanas / meses, e fiquei muito feliz em ter o caso de teste. Quando desenvolvi o método pela primeira vez, testei-o com o método de teste e tive certeza de que funcionaria. Após a alteração, meu código de teste principal ainda funcionava. Então, eu estava certo de que minha mudança não quebrou algum código antigo em produção.

Outro aspecto de escrever testes é mostrar a outros desenvolvedores como usar seu método. Muitas vezes, um desenvolvedor procurará um exemplo de como usar um método e qual será o valor de retorno.

Apenas meus dois centavos .

    
por 01.07.2015 / 19:02
fonte