Erros comuns ao escrever testes de unidade. Palestra Yandex

Se você domina uma pequena lista de erros típicos que ocorrem ao escrever testes de unidade, pode até gostar de escrevê-los. Hoje, o chefe do grupo de desenvolvimento Yandex.Browser para Android, Konstantin kzaikin Zaikin, compartilhará sua experiência com os leitores da Habr.


Eu tenho um relatório prático. Espero que isso beneficie todos vocês - aqueles que já estão escrevendo testes de unidade, e aqueles que estão apenas pensando em escrever, e aqueles que estão tentando e que não tiveram sucesso.

Temos um projeto bem grande. Um dos maiores projetos móveis da Rússia. Temos muito código, muitos testes. Os testes são perseguidos em cada solicitação de pool, eles não caem ao mesmo tempo.

Quem sabe que cobertura de teste ele tem no projeto? Zero, tudo bem. Quem tem testes de unidade no projeto? E quem acredita que testes de unidade não são necessários? Não vejo nada de errado nisso, há pessoas sinceramente convencidas disso, e minha história deve ajudá-las a se convencer disso.

Felizmente, milhares de testes ecológicos - não viemos imediatamente. Não há bala de prata, e a principal idéia do meu relatório na tela:



O ditado chinês está escrito em hieróglifos que uma jornada de mil ou mais começa com um passo. Parece que existe um análogo desse ditado.

Decidimos há muito tempo que precisamos melhorar nosso produto, nosso código e estamos caminhando para isso de propósito. Dessa maneira, encontramos muitas inchaços, um ancinho subaquático, e reunimos algumas crenças.



Por que precisamos de testes?

Para que os recursos antigos não caiam quando introduzimos novos. Para ter um selo no GitHub. Para refatorar os recursos existentes - um pensamento profundo, ele precisa ser revelado para aqueles que não escrevem testes. Para que os recursos existentes não caiam durante a refatoração, nos protegeremos com testes. Para o chefe enviar uma solicitação de pool, sim.

Minha opinião - por favor, não a associe à opinião da minha equipe - de que os testes nos ajudam. Eles permitem que você execute seu código sem colocá-lo em produção, sem instalá-lo em dispositivos, você o inicia e executa muito rapidamente. Você pode executar todos os casos de esquina que você não obtém na vida útil do dispositivo e na produção, e seu testador não os encontrará. Mas você, como desenvolvedor, irá inventá-los, verificá-los e corrigir erros em um estágio inicial.

Muito importante: os testes informam como, de acordo com o desenvolvedor, o código deve funcionar e o que, de acordo com o desenvolvedor, seus métodos devem fazer. Estes não são comentários que se afastam e, depois de um tempo, os úteis se tornam prejudiciais. Acontece que nos comentários uma coisa está escrita e o código é completamente diferente. Os testes de unidade nesse sentido não podem mentir. Se o teste for verde, ele documenta o que está acontecendo lá. O teste falhou - você violou a intenção principal do desenvolvedor.

Comprometer contratos. Estes não são contratos assinados e carimbados, mas contratos de software para comportamento de classe. Se você refatorar, nesse caso, os contratos serão violados e os testes cairão se você os quebrar. Se os contratos forem salvos, os testes permanecerão verdes, você terá mais confiança de que sua refatoração está correta.



Esta é a ideia geral de todo o meu relatório. Você pode mostrar a primeira linha e sair.

Muitas pessoas pensam que o código de teste é mais ou menos, não é para produção, então você pode escrevê-lo. Eu discordo totalmente disso e acho que os testes devem ser abordados antes de tudo de forma responsável, bem como o código de produção. Se você abordá-los da mesma maneira, os testes o beneficiarão. Caso contrário, será uma mancha.

Mais especificamente, as duas linhas abaixo se referem a qualquer código, ao que parece.

BEIJO - mantenha simples, estúpido. Não há necessidade de complicar. Os testes devem ser simples. E o código de produção deve ser simples, mas os testes são especialmente. Se você tiver testes fáceis de ler, provavelmente serão bem escritos, bem expressos, fáceis de testar. Mesmo durante a solicitação do pool, uma pessoa que analisa seus novos testes entenderá o que você queria dizer. E se algo quebrar, você pode entender facilmente o que aconteceu.

SECA - não se repita. Nos testes, o desenvolvedor costuma usar a técnica proibida que ninguém parece usar na produção - copiar e colar. Na produção de um desenvolvedor que copia e cola ativamente, eles simplesmente não entendem. Nos testes, essa é uma prática normal, infelizmente. Não há necessidade de fazer isso, porque - a primeira linha. Se você escrever os testes honestamente, como um código realmente bom, os testes serão úteis para você.

Enquanto desenvolvíamos centenas de milhares de linhas de código, escrevendo milhares de testes, coletando rakes, acumulei comentários típicos sobre os testes. Sou muito preguiçoso e, quando fui às solicitações de pool e observei os mesmos erros, com base no princípio DRY, decidi anotar esses problemas típicos, e fiz isso primeiro no Wiki interno e depois publiquei cheiros práticos de teste no GitHub que você pode seguir quando você escreve testes.



Vou listar por pontos. Incremente um contador em sua mente se você se lembrar de um cheiro de teste. Se você contar até cinco, poderá levantar a mão e gritar "Bingo!" E no final, eu me pergunto quem contou quanto. Meu contador será igual ao número de pontos, eu mesmo os colecionei.


Link do GitHub

A coisa mais difícil de programar que você conhece. E nos testes isso é realmente importante. Se você não nomear bem o teste, provavelmente não poderá formular o que o teste verifica.

Os seres humanos são criaturas bastante simples, são facilmente presos em nomes. Portanto, peço que você chame bem os testes. Formule um teste para verificar e seguir regras simples.

no_action_or_assertion


Se o nome do teste não contiver uma descrição do que o teste verifica, por exemplo, você tem a classe Controller e escreve o teste testController, o que você verifica? O que esse teste deve fazer? Muito provavelmente, nada ou muitas coisas para verificar. Nem um nem outro nos convém. Portanto, no nome do teste, você precisa escrever o que verificamos.

long_name


Você não pode ir para o outro extremo. O nome do teste deve ser curto o suficiente para que uma pessoa possa analisá-lo facilmente. Nesse sentido, o Kotlin é ótimo porque permite escrever nomes de teste entre aspas com espaços em inglês normal. Eles são mais fáceis de ler. Mas ainda assim, nomes longos são cheiro.

Se o nome do seu teste for muito longo, você provavelmente colocará muitos métodos de teste em uma classe de teste e precisará esclarecer o que está verificando. Nesse caso, você precisa dividir sua classe de teste em várias. Não precisa ter medo disso. Você terá um nome de classe de teste que verifica o nome do seu código de produção e haverá nomes curtos de teste.

older_prefix


Isso é atavismo. Anteriormente, em Java, todos testavam usando JUnit, onde até a quarta versão havia um acordo de que os métodos de teste deveriam começar com a palavra teste. Aconteceu, todo mundo ainda chama assim. Mas há um problema, em inglês a palavra test é o verbo "check". As pessoas são facilmente capturadas nessa armadilha e não escrevem mais nenhum outro verbo. Escreva testController. É fácil verificar a si mesmo: se você não escreveu um verbo, o que sua classe de teste deveria fazer, provavelmente você não verificou algo, não o escreveu bem o suficiente. Portanto, sempre solicito que você remova a palavra teste dos nomes dos métodos de teste.

Eu digo coisas muito simples, mas, curiosamente, elas ajudam. Se os testes forem bem-chamados, provavelmente sob o capô eles ficarão bem. É muito simples



Na verdade, eu li ids de cheiros de teste como no GitHub. O link está abaixo, você pode andar e usar.

multiple_asserts


No método de teste, existem muitas afirmações. Então, talvez ou não? Talvez. Isso é bom ou ruim? Eu acho que isso é muito ruim. Se você escreveu várias asserções em um método de teste, verifica várias instruções. Se você testar seu teste e a primeira afirmação cair, o teste alcançará a segunda afirmação? Não chegará. Você já após a queda de sua montagem em algum lugar do IC consegue que o teste caísse, vá consertar algo, preencha novamente, ele cairá na próxima afirmação. Poderia muito bem ser.

Nesse caso, seria muito mais interessante se você usasse esse método de teste em vários, e todos os métodos com várias afirmações caíssem ao mesmo tempo, porque seriam lançados independentemente um do outro.

Mais algumas asserções podem mascarar as diferentes ações que são executadas com a classe de teste. Eu recomendo escrever um teste - um afirmar. Os ativos podem ser bastante complicados. Meu colega, no primeiro relatório, demonstrou um código em que ele usou a excelente afirmação de que a construção e o combinador. Eu realmente amo combates no JUnit, então você pode usá-lo também. Para o leitor de teste, acaba sendo apenas uma declaração curta. O GitHub tem exemplos de todos esses odores e como corrigi-los. Há um exemplo de código incorreto e algum código válido. Tudo isso é feito na forma de um projeto que você pode baixar, abrir, compilar e executar todos os testes.

many_tests_in_one


O próximo cheiro está intimamente relacionado ao anterior. Você faz algo com o sistema - você faz uma afirmação. Fazendo outra coisa com o sistema, algumas operações longas - fazendo uma afirmação - fazendo outra coisa. De fato, você simplesmente viu vários métodos e obtém bons e sólidos métodos de teste.

repeating_setup


Isso se refere à verbosidade. Se você tem uma classe de teste e cada método de teste executa os mesmos métodos no início.

Uma classe de teste na qual os mesmos métodos são executados no início. Parece um pouco, mas em todo método de teste esse lixo está presente. E se é comum a todos os métodos de teste, por que não levá-lo ao construtor ou bloco Antes ou Bloco Antes de Cada na JUnit 5. Se você fizer isso, a legibilidade de cada método melhorará, além de você se livrar do pecado SECO. Tais testes são mais fáceis de manter e mais fáceis de ler.



A confiabilidade dos testes é muito importante. Existem sinais pelos quais se pode determinar que o teste irá chorar, seja verde ou vermelho. Quando o desenvolvedor escreve, ele tem certeza de que é verde e, por algum motivo, os testes ficam verdes ou vermelhos, o que nos dá dor e incerteza em geral de que os testes são úteis. Não temos certeza dos testes, o que significa que não temos certeza de que sejam úteis.

aleatório


Eu próprio escrevi testes que continham Math.random (), faziam números aleatórios, faziam algo com eles. Não há necessidade de fazer isso. Esperamos que o sistema de teste entre no sistema de teste na mesma configuração e a saída dele também deve ser a mesma. Portanto, em testes de unidade, por exemplo, você nunca precisa executar nenhuma operação com a rede. Como o servidor pode não responder, pode haver horários diferentes, outra coisa.

Se você precisar de um teste que funcione com a rede, faça um proxy local, qualquer coisa, mas em nenhum caso vá para uma rede real. Isso é o mesmo aleatório. E, claro, você não pode usar dados aleatórios. Se você precisar fazer alguma coisa, faça alguns exemplos com condições de contorno, com más condições, mas elas devem ser codificadas.

tread_sleep


Um problema clássico que os desenvolvedores enfrentam ao tentar testar algum tipo de código assíncrono. É que fiz algo no teste e preciso esperar até que seja concluído. Como fazer? Thread.sleep (), é claro.

Há um problema. Quando você desenvolveu seu teste, por exemplo, em algumas máquinas de escrever, ele funciona a certa velocidade. Você executa os testes em outra máquina. E o que acontecerá se o seu sistema não conseguir funcionar durante o tempo Thread.sleep ()? O teste fica vermelho. Isso é inesperado. Portanto, a recomendação aqui é que, se você estiver executando operações assíncronas, não as teste. Quase qualquer operação assíncrona pode ser implantada para que você tenha algum tipo de mecanismo condicional que forneça operação assíncrona e um bloco de código executado de forma síncrona. Por exemplo, o AsyncTask possui um bloco de código executado de forma síncrona. Você pode testá-lo facilmente de forma síncrona, sem nenhum assincronismo. Não há necessidade de testar o próprio AsyncTask, é uma classe de estrutura, por que testá-lo? Suporte e sua vida será mais fácil.

Thread.sleep () é muito doloroso. Além de piorar a confiabilidade dos testes, pois permite que eles chorem por causa de diferentes tempos nos dispositivos, isso também diminui a execução dos testes. Quem gostaria que seus testes de unidade, que deveriam ser executados em milissegundos, fossem executados por cinco segundos, porque eu coloquei o sono em marcha?

modify_global


O cheiro típico é que alteramos algum tipo de variável estática global no início do teste para verificar se nosso sistema está funcionando corretamente, mas não retornamos no final. Então, temos uma situação interessante: na máquina, o desenvolvedor executou os testes em uma sequência, primeiro verificou a variável global com o valor padrão, depois, no teste, ele a alterou e depois fez outra coisa. Ambos os testes são verdes. E no CI, aconteceu que os testes começaram na ordem inversa. E um ou ambos os testes serão vermelhos, embora todos sejam verdes.

Você precisa limpar depois de si mesmo. Regras do escoteiro neste sentido: alterou a variável global - retorne ao estado original. Melhor ainda, verifique se os estados globais não são usados. Mas este é um pensamento mais profundo. É sobre o fato de que os testes às vezes destacam defeitos na arquitetura. Se tivermos que mudar estados globais e retorná-los ao estado original para escrever testes, estamos todos bem na nossa arquitetura? Nós realmente precisamos de variáveis ​​globais, por exemplo? Como regra, você pode ficar sem eles injetando algumas classes de contextos ou algo assim, para que você possa reinicializar, injetar e reinicializá-los sempre que no teste.

@VisibleForTesting


Teste de cheiro para avançado. A necessidade de usar tal coisa não surge no primeiro dia, como regra. Você já testou alguma coisa e precisou traduzir a classe em algum estado específico. E você se torna um backdoor. Você tem uma classe de produção e cria um método específico que nunca será chamado em produção e, por meio dela, injeta algo na classe ou altera seu estado. Assim, quebrando maliciosamente o encapsulamento. Na produção, sua classe funciona de alguma forma, mas nos testes, na verdade, é uma classe diferente, você se comunica com ela através de outras entradas e saídas. E aqui você pode obter uma situação em que você altera a produção, mas os testes não percebem. Os testes continuam a atravessar a porta dos fundos e não notaram que, por exemplo, as exceções começaram a disparar no construtor, uma vez que passam por outro construtor.

Em geral, você deve testar suas classes com as mesmas entradas e saídas da produção. Não deve haver acesso a nenhum método apenas para testes.



Quantos de nossos 15 mil testes são realizados? Cerca de 20 minutos, a cada solicitação de pool, no Team City, os desenvolvedores são forçados a esperar. Só porque 15 mil são muitos testes. E nesta seção, compilei cheiros que atrasam os testes. Embora o thread_sleep já estivesse lá.

desnecessary_android_test


O Android tem testes de instrumentação, eles são lindos, executados em um dispositivo ou emulador. Isso elevará seu projeto completamente, de verdade, mas eles são muito lentos. E para eles você precisa até criar um emulador inteiro. Mesmo se você imaginar que possui um emulador levantado no CI - coincide apenas com um -, a execução do teste no emulador levará muito mais tempo do que na máquina host, por exemplo, usando Robolectric. Embora existam outros métodos. Essa é uma estrutura que permite trabalhar com classes da estrutura do Android na máquina host, em Java puro. Nós o usamos bastante ativamente. Anteriormente, o Google era um pouco legal sobre isso, mas agora os próprios googlers falam sobre isso em vários relatórios, é recomendado para uso.

desnecessary_robolectric


A estrutura Android do Robolectric é emulada. Não está completo lá, embora a implementação seja mais longe, mais completa. É um Android quase real, rodando apenas no seu desktop, laptop ou CI. Mas também não precisa ser usado em qualquer lugar. Robolectric não é gratuito. Se você tem um teste que você heroicamente transferiu da instrumentação Android para o Robolectric, você deve pensar - talvez vá ainda mais longe, se livrar do Robolectric, transformá-lo no teste JUnit mais simples? Os testes robolétricos levam tempo para inicializar, tentar carregar recursos, inicializar sua atividade, aplicativo e tudo mais. Isso leva algum tempo. Isso não é um segundo, são milissegundos, às vezes dezenas e centenas. Mas quando existem muitos testes, mesmo isso importa.

Existem técnicas que se livram do Robolectric. Você pode isolar seu código por meio de interfaces, agrupando toda a parte da plataforma com interfaces. Depois, haverá apenas um teste de host JUnit. O JUnit na máquina host é muito rápido, há uma quantidade mínima de sobrecarga; esses testes podem ser executados aos milhares e dezenas de milhares; eles executarão um minuto, alguns minutos. Infelizmente, nossos testes são executados por um longo tempo porque temos muitos testes de instrumentação Android, porque temos uma parte nativa no navegador e somos forçados a executá-los em um emulador ou dispositivo real. Por que tanto tempo.

Eu não vou mais aborrecer você. Quantos cheiros você tem? Até agora, sete no máximo. Inscreva-se no canal , coloque as estrelas.

Source: https://habr.com/ru/post/pt436850/


All Articles