Código testável é melhor código?

103

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.

WannabeCoder
fonte
24
Há uma propriedade especial da função que requer que você passe o tempo chamado idempotency. Essa função produzirá o mesmo resultado toda vez que for chamada com um determinado valor de argumento, o que não apenas a torna mais testável, mas também mais compostável e mais fácil de raciocinar.
Robert Harvey
4
Você pode definir "código melhor"? você quer dizer "manutenível"? ", mais fácil de usar sem o COI-Container-Magic"?
K3b
7
Acho que você nunca teve um teste reprovado porque usava a hora real do sistema e o deslocamento do fuso horário foi alterado.
Andy
5
É melhor que um código não testável.
Tulains Córdova 01/07/2015
14
@RobertHarvey Eu não chamaria isso de idempotência, diria que é transparência referencial : se func(X)retornar "Morning", substituir todas as ocorrências de func(X)com "Morning"não mudará o programa (ou seja, chamar funcnão faz outra coisa senão retornar o valor). Idempotency implica quer que func(func(X)) == X(que não é do tipo correcto-), ou que func(X); func(X);executa os mesmos efeitos secundários como func(X)(mas não existem efeitos colaterais aqui)
Warbo

Respostas:

116

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.

gbjbaanb
fonte
3
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
World Engineer
2
Acho que vale a pena notar que conheço pelo menos uma linguagem que permite que você faça o que seu último parágrafo descreve: Python com Mock. Devido à maneira como as importações de módulos funcionam, praticamente qualquer coisa além das palavras-chave pode ser substituída por uma simulação, até mesmo de métodos / classes / API padrão da API. Portanto, isso é possível, mas pode exigir que a linguagem seja arquitetada de uma maneira que suporte esse tipo de flexibilidade.
Jpmc26 4/07
6
Eu acho que há uma diferença entre "código testável" e "código [distorcido] para se adequar à estrutura de teste". Não sei para onde estou indo com esse comentário, exceto para dizer que concordo que o código "distorcido" é ruim e o código "testável" com boas interfaces é bom.
Bryan Oakley
2
Eu expressei algumas das minhas opiniões nos comentários do artigo (já que comentários extensos não são permitidos aqui), confira! Para ser claro: Eu sou o autor do artigo mencionado :)
Sergey Kolodiy
Tenho que concordar com @BryanOakley. O "código testável" sugere que suas preocupações são separadas: é possível testar um aspecto (módulo) sem interferência de outros aspectos. Eu diria que isso é diferente de "ajustar as convenções de teste específicas do suporte ao seu projeto". Isso é semelhante aos padrões de design: eles não devem ser forçados. Código que utiliza adequadamente padrões de design seria considerado código forte. O mesmo se aplica aos princípios de teste. Se tornar seu código "testável" resultar em distorções excessivas no código do seu projeto, você estará fazendo algo errado.
Vince Emigh
68

Escrever código testável ainda é uma boa prática, mesmo na ausência de testes?

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:

  1. Faça com que funcione - se o código não fizer o que precisa, será inútil.
  2. Torne-o sustentável - se o código não for sustentável, ele irá parar de funcionar rapidamente.
  3. Torne-o flexível - se o código não for flexível, ele parará de funcionar quando os negócios surgirem e perguntará se o código pode executar XYZ.
  4. Faça com que seja rápido - além de um nível aceitável básico, o desempenho é apenas um molho.

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.

Telastyn
fonte
4
Gosto da relação que você aponta entre testável e flexível - que torna todo o problema mais compreensível para mim. A flexibilidade permite que seu código se adapte, mas necessariamente o torna um pouco mais abstrato e menos intuitivo de entender, mas esse é um sacrifício que vale a pena pelos benefícios.
precisa saber é o seguinte
3
Dito isso, muitas vezes vejo métodos que deveriam ter sido privados sendo forçados a nível público ou de pacote, para que a estrutura de teste de unidade possa acessá-los diretamente. Longe de uma abordagem ideal.
Jwenting
4
@WannabeCoder Obviamente, só vale a pena adicionar flexibilidade quando você economizar tempo no final. É por isso que não escrevemos todos os métodos em uma interface - na maioria das vezes é apenas mais fácil reescrever o código em vez de incorporar muita flexibilidade desde o início. O YAGNI ainda é um princípio extremamente poderoso - apenas certifique-se de que seja o que for "você não vai precisar", adicioná-lo retroativamente não lhe dará mais trabalho em média do que implementá-lo antes do tempo. É o código que não segue o YAGNI que tem mais problemas com flexibilidade na minha experiência.
Luaan
3
"Não ter testes de unidade significa que você não terminou seu código / recurso" - Falso. A "definição de pronto" é algo que a equipe decide. Pode ou não incluir algum grau de cobertura de teste. Mas em nenhum lugar existe um requisito estrito que diz que um recurso não pode ser "concluído" se não houver testes para ele. A equipe pode optar por exigir testes ou não.
Aroth
3
@Telastyn Em mais de 10 anos de desenvolvimento, nunca tive uma equipe que exigisse uma estrutura de teste de unidade, e apenas duas que possuíam uma (ambas tinham pouca cobertura). Um local exigia um documento do Word de como testar o recurso que você estava escrevendo. É isso aí. Talvez eu tenha azar? Não sou anti-unit unit (sério, modifico o site SQA.SE, sou muito pro unit unit!), Mas não os achei tão difundidos quanto a sua declaração afirma.
precisa saber é o seguinte
50

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.

Kilian Foth
fonte
Outro exemplo é se você deseja extrair parte do código de um projeto para outro lugar (por qualquer motivo). Quanto mais as diferentes partes da funcionalidade são independentes, mais fácil é extrair exatamente a funcionalidade necessária e nada mais.
#
10
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.
jcollum
Eu tenho lido lentamente sobre práticas de teste por um tempo agora (como de alguma forma quem ainda não faz testes de unidade) e devo dizer, a última parte sobre a verificação da chamada em circunstâncias controladas e o código mais flexível que vem com fez com que todos os tipos de coisas se encaixassem. Obrigado.
precisa saber é o seguinte
12

Em algum momento, o valor precisa ser inicializado e por que não estar mais próximo do consumo?

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.

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.

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á.

utnapistim
fonte
10

Pode parecer bobagem dizê-lo dessa maneira, mas se você quiser testar seu código, sim, escrever código testável é melhor. Você pergunta:

Em algum momento, o valor precisa ser inicializado e por que não estar mais próximo do consumo?

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.

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 simplificará o código acima, com o efeito colateral de ser testado sistematicamente.

Eric King
fonte
1
Então você teria que testar de manhã cedo para verificar se obtém o resultado de "Noite" quando quiser. Isso é difícil. Agora, suponha que você queira verificar se a manipulação de datas está correta em 29 de fevereiro de 2016 ... E alguns programadores do iOS (e provavelmente outros) são atormentados por um erro de iniciante que estraga tudo um pouco antes ou depois do início do ano, como você teste para isso. E com a experiência, gostaria de verificar manuseio data em 02 de fevereiro de 2020.
gnasher729
1
@ gnasher729 Exatamente o que quero dizer. "Tornar este código testável" é uma alteração simples que pode resolver muitos problemas (de teste). Se você não deseja automatizar o teste, acho que o código é aceitável como está. Mas seria melhor quando for "testável".
Eric King
9

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 GetTimeOfDayseja mais testável, você está de volta ao mesmo barco testando o seu novo IDateTimeProvider. 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.

Karl Bielefeldt
fonte
+1 - você precisa melhorar o design e a arquitetura para facilitar a escrita dos testes de unidade.
BЈовић
3
+ - é a arquitetura do seu código que importa. Testes mais fáceis são apenas um efeito colateral feliz.
Gbjbaanb
8

É 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.

Carl Manaster
fonte
discordo de ser DRY - envolver GetCurrentTime em um método MyGetCurrentTime está repetindo muito a chamada do sistema operacional sem nenhum benefício, exceto para ajudar as ferramentas de teste. Esse é apenas o exemplo mais simples, eles ficam muito piores na realidade.
Gbjbaanb
1
"repetindo a chamada do sistema operacional sem benefício" - até você terminar em um servidor com um relógio, falando com um servidor aws em um fuso horário diferente, e isso quebra seu código, e você precisa passar por todo o código e atualize-o para usar o MyGetCurrentTime, que retorna UTC. ; inclinação do relógio, horário de verão e há outras razões pelas quais pode não ser uma boa ideia confiar cegamente na chamada do sistema operacional ou, pelo menos, ter um único ponto em que você poderá usar outro substituto.
Andrew Hill
8

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...

Craig
fonte
1
"código melhor tende a ser também código testável" Essa é a chave. Tornar testável não o torna melhor. Melhorá-lo com frequência o torna testável, e os testes geralmente fornecem informações que você pode usar para melhorá-lo, mas a mera presença de testes não implica qualidade e existem exceções (raras).
Anaximander 02/07/2015
1
Exatamente. Considere o contrapositivo. Se é um código não testável, não é testado. Se não foi testado, como você sabe se funciona ou não em uma situação real?
Pjc50
1
Todos os testes comprovam que o código passa nos testes. Caso contrário, o código testado em unidade não apresentaria erros e sabemos que não é esse o caso.
Wobily_col
@anaximander Exatamente. Há pelo menos a possibilidade de que a mera presença de testes seja uma contra-indicação que resulte em código de qualidade inferior se todo o foco estiver apenas nas caixas de seleção. "Pelo menos sete testes de unidade para cada função?" "Verifica." Mas eu realmente acredito que, se o código for de qualidade, será mais fácil testar.
Craig
1
... mas fazer da burocracia um teste pode ser um desperdício total e não produzir informações úteis ou resultados confiáveis. Independentemente; Eu com certeza gostaria que alguém tivesse testado o bug SSL Heartbleed , sim? ou o Apple vai falhar bug?
Craig
5

Para mim, no entanto, parece um exagero passar no tempo como argumento.

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:

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

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):

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 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 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:

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

Código do 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 oferece vários benefícios:

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

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_daycomeça a usar outra fonte por um tempo.

l0b0
fonte
FYI: seu argumento padrão está errado. Ele será definido apenas uma vez, portanto, sua função sempre retornará a hora em que foi avaliada pela primeira vez.
ahruss
4

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.

cbojar
fonte
4

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.

Alex D
fonte
1

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 .

BtD
fonte