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 pergunta aborda os princípios do SOLID de escrever código testável, mas quero saber se esses princípios de design são benéficos (ou pelo menos não prejudiciais) sem o planejamento de escrever testes. Para esclarecer - eu entendo a importância de escrever testes; Esta não é uma pergunta sobre a sua utilidade.
Para ilustrar minha confusão, na peça que inspirou essa pergunta, 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 incorreto, porque produz os dados (o tempo) que usa internamente, dificultando o teste. Para mim, no entanto, parece um exagero passar no tempo como argumento. Em algum momento, o valor precisa ser inicializado e por que não estar mais próximo do consumo? Além disso, o objetivo do método em minha mente é retornar algum valor com base no tempo atual , tornando-o um parâmetro que você implica que esse objetivo pode / deve ser alterado. Isso e outras perguntas me levaram a pensar se código testável era sinônimo de código "melhor".
Escrever código testável ainda é uma boa prática, mesmo na ausência de testes?
O código testável é realmente mais estável? foi sugerido como duplicado. No entanto, essa pergunta é sobre a "estabilidade" do código, mas estou perguntando de maneira mais ampla se o código também é superior por outros motivos, como legibilidade, desempenho, acoplamento etc.
fonte
func(X)
retornar"Morning"
, substituir todas as ocorrências defunc(X)
com"Morning"
não mudará o programa (ou seja, chamarfunc
não faz outra coisa senão retornar o valor). Idempotency implica quer quefunc(func(X)) == X
(que não é do tipo correcto-), ou quefunc(X); func(X);
executa os mesmos efeitos secundários comofunc(X)
(mas não existem efeitos colaterais aqui)Respostas:
No que diz respeito à definição comum de testes de unidade, eu diria que não. Eu vi códigos simples complicados por causa da necessidade de alterá-los para se adequar à estrutura de teste (por exemplo, interfaces e IoC em todos os lugares, dificultando o acompanhamento através de camadas de chamadas e dados de interface que devem ser obviamente transmitidos por mágica). Dada a escolha entre o código que é fácil de entender ou o código que é fácil para o teste de unidade, eu sempre uso o código de manutenção.
Isso não significa não testar, mas ajustar 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 ruim). Por exemplo, você pode criar testes de unidade menos granulares (por exemplo , a atitude de Martin Fowler de que uma unidade geralmente é uma classe, não um método), ou você pode acessar seu programa com testes de integração automatizados. Isso pode não ser tão bonito quanto a sua estrutura de teste acende com tiques verdes, mas estamos após o código testado, não a gamificação do processo, certo?
Você pode facilitar a manutenção do seu código e ainda ser bom para testes de unidade, definindo boas interfaces entre eles e depois escrevendo testes que exercem a interface pública do componente; ou você pode obter uma estrutura de teste melhor (uma que substitua funções em tempo de execução para zombar delas, em vez de exigir que o código seja compilado com zombarias). Uma estrutura de teste de unidade melhor permite substituir a funcionalidade GetCurrentTime () do sistema pela sua, em tempo de execução, para que você não precise introduzir invólucros artificiais apenas para se adequar à ferramenta de teste.
fonte
Primeiramente, a ausência de testes é um problema muito maior do que seu código sendo testável ou não. Não ter testes de unidade significa que você não terminou seu código / recurso.
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; portanto, há muita sobreposição na abordagem e no que as pessoas chamam.
Então, para mim, sempre há um conjunto de prioridades na escrita de código:
Os 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 fazer os testes de unidade funcionarem, isso será contraproducente. "Código testável" geralmente é um código flexível, o que é bom, mas não tão importante quanto a funcionalidade ou a 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, correto e mais complexo. Como a manutenibilidade é mais importante, usualmente errei na abordagem mais simples, mesmo que seja menos testável.
fonte
Sim, é uma boa prática. A razão é que a testabilidade não é feita para testes. É por uma questão de clareza e compreensão que ela traz consigo.
Ninguém se importa com os testes. É um fato triste da vida que precisamos de grandes suítes de testes de regressão, porque não somos brilhantes o suficiente para escrever um código perfeito sem verificar constantemente nosso equilíbrio. 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 mostrou que quase todos nós não podemos, portanto, os testes que abrangem nosso código são bons, mesmo que demorem um tempo para escrever o código comercial.
Como os testes melhoram nosso código comercial independentemente do teste? Forçando-nos a segmentar nossa funcionalidade em unidades que facilmente demonstram estar corretas. Essas unidades também são mais fáceis de acertar do que aquelas que, de outra forma, seríamos tentadas a escrever.
Seu exemplo do tempo é um bom ponto. Contanto que você tenha apenas uma função retornando a hora atual, você pode pensar que não há sentido em programá-la. Quão difícil pode ser fazer isso direito? Mas, inevitavelmente, seu programa usará essa função em outro código, e você definitivamente deseja testá- lo sob diferentes condições, inclusive em momentos diferentes. Portanto, é uma boa idéia poder manipular o tempo que sua função retorna - não porque você desconfia de sua
currentMillis()
chamada de linha única , mas porque precisa verificar os chamadores dessa chamada em circunstâncias controladas. Como você vê, ter código testável é útil, mesmo que por si só, não pareça merecer tanta atenção.fonte
Nobody cares about the tests themselves
-- Eu faço. Acho os testes uma documentação melhor do que o código faz do que quaisquer comentários ou arquivos leia-me.Porque você pode precisar reutilizar esse código, com um valor diferente daquele 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, e não apenas "agora" (com "agora" significando quando você chama o código).
Tornar o código testável de fato significa tornar o 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á uma grande vantagem em escrever código reutilizável, e os dois são sinônimos.
Você também pode argumentar que o objetivo desse método é retornar algum valor com base em um valor de tempo e é necessário gerar isso com base em "agora". 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á.
fonte
Pode parecer bobagem dizê-lo dessa maneira, mas se você quiser testar seu código, sim, escrever código testável é melhor. Você pergunta:
Precisamente porque, no exemplo a que você está se referindo, torna esse código não testável. A menos que você execute apenas um subconjunto de seus testes em diferentes horários do dia. Ou você redefine o relógio do sistema. Ou alguma outra solução alternativa. Tudo isso é pior do que simplesmente tornar seu código flexível.
Além de inflexível, esse pequeno método em questão tem duas responsabilidades: (1) obter a hora do sistema e (2) retornar algum valor com base nela.
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 simplificará o código acima, com o efeito colateral de ser testado sistematicamente.fonte
Certamente, tem um custo, mas alguns desenvolvedores estão tão acostumados a pagar que se esqueceram do custo. Para o seu exemplo, agora você tem duas unidades em vez de uma, solicitou o 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 o seu novoIDateTimeProvider
. Apenas se você tiver bons testes, os benefícios geralmente superam os custos.Além disso, até certo ponto, escrever um código testável incentiva você a arquitetar seu código de maneira mais sustentável. O novo código de gerenciamento de dependências é irritante, portanto, convém agrupar todas as suas funções dependentes do tempo, se possível. Isso pode ajudar a atenuar e corrigir erros, como, por exemplo, quando você carrega uma página no limite de tempo, tendo alguns elementos renderizados no tempo anterior e alguns no período posterior. Também pode acelerar o seu programa, evitando repetidas chamadas do sistema para obter a hora atual.
Obviamente, essas melhorias arquiteturais dependem muito de alguém perceber as oportunidades e implementá-las. Um dos maiores perigos de se concentrar tão de perto nas unidades é perder de vista o quadro geral.
Muitas estruturas de teste de unidade permitem que você conserte um objeto simulado em tempo de execução, o que permite obter os benefícios da testabilidade sem toda a bagunça. Eu já vi isso feito em C ++. Examine essa capacidade em situações em que parece que o custo da testabilidade não vale a pena.
fonte
É possível que nem todas as características que contribuem para a testabilidade sejam desejáveis fora do contexto da testabilidade - tenho problemas para apresentar uma justificativa não relacionada ao teste para o parâmetro de tempo que você cita, por exemplo -, mas de um modo geral as características que contribuem para a testabilidade também contribuem para um bom código, independentemente da capacidade de teste.
Em termos gerais, código testável é código maleável. É em pedaços pequenos, discretos, coesos, para que os bits individuais possam ser chamados para reutilização. É bem organizado e bem nomeado (para poder testar algumas funcionalidades, dê mais atenção à nomeação; se você não estivesse escrevendo testes, o nome de uma função de uso único seria menos importante). Ele tende a ser mais paramétrico (como o exemplo do seu tempo), portanto, aberto para uso de outros contextos além do objetivo original pretendido. É SECO, tão menos confuso e mais fácil de entender.
Sim. É uma boa prática escrever código testável, mesmo independentemente do teste.
fonte
Escrever código testável é importante se você quiser provar que seu código realmente funciona.
Costumo concordar com os sentimentos negativos sobre transformar seu código em contorções hediondas apenas para ajustá-lo a uma estrutura de teste específica.
Por outro lado, todo mundo aqui, em algum momento ou outro, teve que lidar com essa função mágica de 1.000 linhas de comprimento, que é simplesmente hedionda de se lidar, praticamente não pode ser tocada sem quebrar uma ou mais dependências óbvias em algum outro lugar (ou em algum lugar dentro de si, onde a dependência é quase impossível de visualizar) e, por definição, é praticamente testável. A noção (que não é sem mérito) de que as estruturas de teste se tornaram exageradas não deve ser tomada como uma licença gratuita para escrever código de baixa qualidade e não testável, na minha opinião.
Os ideais de desenvolvimento orientado a testes tendem a levá-lo a escrever procedimentos de responsabilidade única, por exemplo, e isso é definitivamente uma coisa boa. Pessoalmente, digo que adquira a responsabilidade única, a fonte única da verdade, o escopo controlado (sem variáveis globais) e mantenha as dependências frágeis no mínimo, e seu código será testável. Testável por alguma estrutura de teste específica? Quem sabe. Mas talvez seja a estrutura de teste que precisa se ajustar ao bom código, e não o contrário.
Mas, para deixar claro, código que é 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, por coincidência, não é um código que pode ser facilmente testado.
Tão próximo do meu resumo, código testável é melhor código?
Eu não sei, talvez não. As pessoas aqui têm alguns pontos válidos.
Mas acredito que um código melhor também tende a ser um código testável .
E se você estiver falando de software sério para uso em empreendimentos sérios, enviar código não testado não é a coisa mais responsável que você poderia estar fazendo com o dinheiro do seu empregador ou dos seus clientes.
Também é verdade que alguns códigos exigem testes mais rigorosos que outros e é um pouco tolo fingir o contrário. Como você gostaria de ser um astronauta no ônibus espacial se o sistema de menus que o interage com os sistemas vitais do ônibus espacial não foi testado? Ou um funcionário de uma usina nuclear em que os sistemas de software que monitoram a temperatura no reator não foram testados? Por outro lado, um pouco de código que gera um relatório simples de leitura requer um caminhão de contêineres cheio de documentação e mil testes de unidade? Eu espero que não. Apenas dizendo...
fonte
Você está certo e, com a zombaria, pode tornar o código testável e evitar passar o tempo (intenção trocadiça indefinida). Código de exemplo:
Agora, digamos que você queira testar o que acontece durante um salto de segundo. Como você diz, para testar isso da maneira exagerada, você precisaria alterar o código (de produção):
Se o Python suportasse segundos bissextos, o código de teste ficaria assim:
Você pode testar isso, mas o código é mais complexo que o necessário e os testes ainda não podem exercer com segurança a ramificação de código que a maioria dos códigos de produção estará usando (ou seja, não passando um valor para
now
). Você soluciona isso usando uma simulação . A partir do código de produção original:Código do teste:
Isso oferece vários benefícios:
time_of_day
independentemente de suas dependências.Em uma nota lateral, espera-se que futuras estruturas de zombaria tornem coisas assim mais fáceis. Por exemplo, como você precisa se referir à função simulada como uma sequência, não é possível fazer com que os IDEs a alterem automaticamente quando
time_of_day
começa a usar outra fonte por um tempo.fonte
Uma qualidade do código bem escrito é que ele é robusto para mudar . Ou seja, quando uma alteração de requisitos ocorre, a alteração no código deve ser proporcional. Esse é um ideal (e nem sempre é alcançado), mas escrever código testável ajuda a nos aproximar desse objetivo.
Por que isso ajuda a nos aproximar? Na produção, nosso código opera dentro do ambiente de produção, incluindo a integração e a interação com todos os outros códigos. Nos testes de unidade, varremos grande parte 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 entradas diferentes (zombarias, entradas ruins que talvez nunca sejam repassadas, etc.) do que as usamos na produção.
Isso prepara nosso código para o dia em que as alterações ocorrem em nosso sistema. Digamos que nosso cálculo de tempo precise levar um tempo diferente com base em um fuso horário. Agora, temos a capacidade de passar esse tempo e não precisamos fazer alterações no código. Quando não queremos passar um tempo e queremos usar o horário atual, podemos apenas usar um argumento padrão. Nosso código é robusto para alterar porque é testável.
fonte
Pela minha experiência, uma das decisões mais importantes e mais abrangentes que você toma ao criar um programa é como você divide o código em unidades (onde "unidades" são usadas em seu sentido mais amplo). 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 dividir o código de cada classe em algum número de métodos. Em alguns idiomas, a escolha é como dividir seu código em funções. Ou, se você faz a coisa SOA, precisa decidir quantos serviços criará e o que entrará em cada serviço.
A discriminação que você escolhe tem um efeito enorme em todo o processo. Boas opções facilitam a gravação do código 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, acontece que, uma vez que você encontra um bom colapso, geralmente também é mais fácil testar do que um ruim.
Porque isto é assim? Acho que não consigo entender e explicar todas as razões. Mas uma razão é que uma boa decomposição significa, invariavelmente, escolher um "tamanho de grão" moderado para as unidades de implementação. Você não deseja incluir muita funcionalidade e muita lógica em uma única classe / método / função / módulo / etc. Isso facilita a leitura e a gravação do seu código, mas também 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 o teste. Um bom design geralmente significa que cada unidade de implementação terá um número moderado de dependências em relação às outras. Isso facilita o código para que outras pessoas leiam e entendam, 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 da 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". Essa pode ser uma boa ideia, mas é uma questão separada do que estou descrevendo aqui e não vou me aprofundar nos prós e contras.
Aliás, mesmo se você fizer chamadas diretas para funções do sistema que retornam a hora atual, atuam no sistema de arquivos e assim por diante, isso não significa que você não pode testar seu código de forma isolada. 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 fazer isso com muitas linguagens e plataformas de desenvolvimento. (Espero que o tempo de execução do seu idioma seja de código aberto e fácil de construir. Se a execução do seu código envolver uma etapa de link, esperemos que também seja fácil controlar em quais bibliotecas ele está vinculado.)
Em resumo, o código testável não é necessariamente "bom", mas o código "bom" geralmente é testável.
fonte
Se você seguir os princípios do SOLID , estará do lado bom, principalmente se estender isso com o KISS , DRY e YAGNI .
Um ponto que falta para mim é o ponto da complexidade de um método. É um método getter / setter simples? Então, apenas escrever testes para satisfazer sua estrutura de testes seria uma perda de tempo.
Se for um método mais complexo no qual você manipula dados e deseja ter certeza de que funcionará mesmo que você precise alterar a lógica interna, seria uma ótima opção para um método de teste. Muitas vezes tive que mudar um pedaço de código após vários dias / semanas / meses e fiquei muito feliz em ter o caso de teste. Quando desenvolvi o método, testei-o com o método de teste e tinha certeza de que funcionaria. Após a alteração, meu código de teste principal ainda funcionou. Então, eu tinha certeza de que minha alteração não quebrou nenhum código antigo na produção.
Outro aspecto de escrever testes é mostrar a outros desenvolvedores como usar seu método. Muitas vezes, um desenvolvedor procura um exemplo de como usar um método e qual será o valor de retorno.
Apenas meus dois centavos .
fonte