Teste de unidade que afirma que a rosca atual é a rosca principal

8

A questão é: faz sentido escrever um teste de unidade que afirme que o thread atual é o thread principal? Prós e contras?

Recentemente, vi o teste de unidade que afirma o segmento atual para o retorno de chamada do serviço. Não tenho certeza, é uma boa ideia, acredito que seja mais um tipo de teste de integração. Na minha opinião, o teste de unidade deve afirmar o método isoladamente e não deve saber sobre a natureza do consumidor de serviço.

Por exemplo, no iOS, o consumidor deste serviço deve ser uma interface do usuário que, por padrão, possui uma restrição para executar um código no encadeamento principal.

Vladimir Kaltyrin
fonte
1
FWIW, em nosso código, em vez de torná-lo uma asserção de teste de unidade, fazemos uma asserção de tempo de execução, já que é onde realmente importa.
user1118321
Parece mais uma afirmação do que um caso de teste real.
Whatsisname

Respostas:

11

Deixe-me mostrar meus princípios favoritos de teste de unidade:

Um teste não é um teste de unidade se:

  1. Ele fala com o banco de dados
  2. Ele se comunica através da rede
  3. Toca no sistema de arquivos
  4. Ele não pode ser executado ao mesmo tempo que qualquer outro teste de unidade
  5. Você precisa fazer coisas especiais no seu ambiente (como editar arquivos de configuração) para executá-lo.

Um conjunto de regras de teste de unidade - Michael Feathers

Se o seu teste de unidade insistir em que seja o "segmento principal", é difícil não encontrar o número 4.

Isso pode parecer difícil, mas lembre-se de que um teste de unidade é apenas um dos muitos tipos diferentes de testes.

insira a descrição da imagem aqui

Você é livre para violar esses princípios. Mas quando o fizer, não chame seu teste de teste de unidade. Não o coloque nos testes reais de unidade e reduza a velocidade.

candied_orange
fonte
Como você faz um teste de unidade em um logger? Você precisa fazer moc IO? Isso parece desagradável.
whn
1
@opa se o seu criador de logs tiver uma lógica comercial interessante, você isola e faz o teste de unidade. O código chato que confirma que temos um sistema de arquivos não precisa de teste de unidade.
candied_orange
@candied_orange Então agora você tem que expor a lógica interna? Porque de que outra forma você vai separar a sequência gerada pelo criador de logs antes que ela seja enviada para um arquivo com a separação em um método privado ?
whn
@opa Se você precisar testar se pode fazê-lo, passe no seu próprio fluxo. A unidade testa unidades de teste, não recursos do sistema.
Candied_orange
@candied_orange, exceto quando o criador de logs usa apenas o nome do arquivo para o qual deve enviar como entrada. O ponto não é que "A unidade testa unidades de teste e não os recursos do sistema" está errada, é 100% correta. Você não pode ser tão dogmático ao dizer quando os testes tocam o arquivo io que não é um teste unitário válido. Testar um criador de logs através da leitura de sua saída de arquivo é muito mais fácil do que tornar pública a funcionalidade privada apenas para fins de teste de unidade, o que você realmente nunca deve fazer . Não está testando o IO, está testando o criador de logs, apenas a maneira mais fácil de fazer isso é ler o arquivo se você ainda deseja um código limpo.
whn
5

Você deve escrever um teste de unidade que testa se um fio é o fio principal? Depende, mas provavelmente não é uma boa ideia.

Se o objetivo de um teste de unidade se destina a verificar o funcionamento adequado de um método, testar esse aspecto quase certamente não está testando esse aspecto. Como você mesmo disse, é conceitualmente mais um teste de integração, já que não é provável que o funcionamento adequado de um método mude com base em qual thread o está executando, e o responsável por determinar qual thread será usado é o chamador e não o próprio método.

No entanto, é por isso que digo que depende, porque pode muito bem depender do método em si, caso o método gere novos threads. Embora com toda a probabilidade esse não seja o seu caso.

Meu conselho seria deixar essa verificação fora do teste de unidade e portá-la para um teste de integração adequado para esse aspecto.

Neil
fonte
2

não deve saber sobre a natureza do consumidor de serviço.

Quando um consumidor consome um serviço, existe um "contrato" expresso ou implícito sobre qual funcionalidade é fornecida e como. Restrições sobre em qual thread o retorno de chamada pode ocorrer fazem parte desse contrato.

Presumo que seu código seja executado em um contexto em que haja algum tipo de "mecanismo de eventos" executado no "encadeamento principal" e uma maneira de outros encadeamentos solicitarem que o código de agendamentos do mecanismo de eventos seja executado no encadeamento principal.

Na visão mais pura dos testes de unidade, você zombaria absolutamente de toda dependência. No mundo real, poucas pessoas fazem isso. O código em teste pode usar as versões reais de pelo menos recursos básicos da plataforma.

Portanto, a verdadeira questão da IMO é que você considera o mecanismo de eventos e o suporte de threading em tempo de execução como parte dos "recursos básicos da plataforma" dos quais os testes de unidade podem depender ou não? Se você fizer isso, faz todo o sentido verificar se um retorno de chamada ocorre no "encadeamento principal". Se você estiver zombando do mecanismo de eventos e do sistema de encadeamento, projete suas zombarias para que eles possam determinar se o retorno de chamada teria acontecido no encadeamento principal. Se você estiver simulando o mecanismo de eventos, mas não o suporte ao encadeamento em tempo de execução, você deseja testar se o retorno de chamada ocorre no encadeamento em que o mecanismo de eventos simulados é executado.

Peter Green
fonte
1

Eu posso ver por que seria um teste atraente. Mas o que realmente estaria testando? diga que temos a função

showMessage(string m)
{
    new DialogueBox(m).Show(); //will fail if not called from UI thread
}

Agora eu posso testar isso e executá-lo em um thread que não seja da interface do usuário, provando que um erro é gerado. Mas, realmente, para que esse teste tenha algum valor, a função precisa ter algum comportamento específico que você deseja que seja executado quando executado em um thread que não seja da interface do usuário.

showMessage(string m)
{
    try {
        new DialogueBox(m).Show(); //will fail if not called from UI thread
    }
    catch {
        Logger.Log(m); //alternative display method
    }
}

Agora eu tenho algo para testar, mas não está claro que esse tipo de alternativa seria útil na vida real

Ewan
fonte
1

Se o retorno de chamada estiver documentado para não ser seguro para thread invocar outro que não seja, por exemplo, o thread da interface do usuário ou o thread principal, parece perfeitamente razoável em um teste de unidade de um módulo de código que faz com que a chamada desse retorno de chamada garanta que não está fazendo algo que não é seguro para threads, trabalhando fora do thread que o SO ou a estrutura do aplicativo está documentada para garantir a segurança.

hotpaw2
fonte
1

Organizar declaração de
ato

Um teste de unidade deve afirmar que está em um segmento específico? Não, porque não está testando uma unidade nesse caso, nem sequer é um teste de integração, pois não diz nada sobre a configuração que está ocorrendo na unidade ...

Afirmar em qual thread o teste está, seria como Afirmar o nome do seu servidor de teste ou, melhor ainda, o nome do método de teste.

Agora, pode fazer sentido organizar para que o teste seja executado em um encadeamento específico, mas somente quando você quiser ter certeza de que retornará um resultado específico com base no encadeamento.

jmoreno
fonte
1

Se você possui uma função documentada para ser executada apenas corretamente no thread principal e essa função é chamada em um thread em segundo plano, essa não é a falha das funções. A falha é culpa do chamador, portanto, os testes de unidade são inúteis.

Se o contrato for alterado para que a função seja executada corretamente no encadeamento principal e relatar uma falha ao ser executado em um encadeamento em segundo plano, você pode escrever um teste de unidade para isso, onde é chamado uma vez no encadeamento principal e depois em um thread de segundo plano e verifique se ele se comporta conforme documentado. Portanto, agora você tem um teste de unidade, mas não um teste de unidade, verificando se ele é executado no thread principal.

Eu sugeriria muito que a primeira linha da sua função afirmasse que ela é executada no thread principal.

gnasher729
fonte
1

O teste de unidade é útil apenas se estiver testando a implementação correta da referida função, não o uso correto, porque um teste de unidade não pode dizer que uma função não será chamada de outro thread no restante da sua base de código, da mesma forma que pode ' diga que funções não serão chamadas com parâmetros que violam suas condições prévias (e tecnicamente o que você está tentando testar é basicamente uma violação de uma pré-condição em uso, algo que você não pode testar efetivamente porque o teste pode ' para restringir como outros lugares na base de código usam essas funções; você pode testar se violações de pré-condições resultam em erros / exceções apropriados).

Este é um caso de afirmação para mim na implementação das funções relevantes, como algumas pessoas apontaram, ou ainda mais sofisticado é garantir que as funções sejam seguras contra threads (embora isso nem sempre seja prático quando se trabalha com algumas APIs).

Também apenas uma nota lateral, mas "thread principal"! = "Thread da interface do usuário" em todos os casos. Muitas APIs da GUI não são seguras para threads (e fazer um kit GUI seguro para threads é muito difícil), mas isso não significa que você precise invocá-las a partir do mesmo thread que aquele que tem o ponto de entrada para o seu aplicativo. Isso pode ser útil mesmo na implementação de suas asserções dentro das funções relevantes da interface do usuário para distinguir "thread da interface do usuário" do "thread principal", como capturar o ID do thread atual quando uma janela é criada para comparar, em vez do ponto de entrada principal do aplicativo (que pelo menos reduz a quantidade de suposições / restrições de uso que a implementação está aplicando apenas ao que é realmente relevante).

A segurança do encadeamento foi, na verdade, o ponto de partida de "pegadinha" em uma antiga equipe minha e, no nosso caso particular, eu a teria rotulado como a "micro-otimização" mais contraproducente de todos, de um tipo que incorreu em mais custos de manutenção do que nunca montagem manuscrita. Tivemos uma cobertura de código bastante abrangente em nossos testes de unidade, juntamente com testes de integração bastante sofisticados, apenas para encontrar conflitos e condições de corrida no software que escapou aos nossos testes. E isso aconteceu porque os desenvolvedores aleatoriamente multithreaded code sem estarem cientes de todos os efeitos colaterais que poderiam ocorrer na cadeia de chamadas de funções que resultariam de seus próprios, com uma idéia bastante ingênua de que eles poderiam corrigir esses erros em retrospectiva apenas jogando trava em torno de esquerda e direita,

Eu estava inclinado na direção oposta, como um tipo da velha escola que desconfiava de multithreading, era um verdadeiro retardatário em adotá-lo, e pensei que a correção supera o desempenho a ponto de raramente se aproveitar de todos esses núcleos que temos agora, até descobrir coisas. como funções puras, designs imutáveis ​​e estruturas de dados persistentes que finalmente me permitiram utilizar totalmente esse hardware sem me preocupar com as condições de corrida e os impasses. Devo admitir que até 2010, mais ou menos, eu odiava multithreading com paixão, exceto por alguns loops paralelos aqui e ali em áreas que são triviais para raciocinar sobre segurança de threads e favorecia um código muito mais seqüencial para o design de produtos, dada a minha dor com multithreading em equipes anteriores.

Para mim, esse modo de multithreading primeiro e corrigir erros mais tarde é uma estratégia terrível para multithreading a ponto de quase me fazer odiar o multithreading inicialmente; você assegura que seus projetos sejam seguros para encadeamento e que suas implementações usem apenas funções com garantias semelhantes (por exemplo: funções puras) ou evite multithreading. Isso pode parecer um pouco dogmático, mas é melhor do que descobrir (ou pior, não descobrir) problemas difíceis de reproduzir em retrospectiva que escapam aos testes. Não faz sentido otimizar um motor de foguete se isso resultar em uma explosão inesperada inesperada do nada no meio do caminho ao longo de sua jornada para o espaço.

Se você inevitavelmente precisar trabalhar com código que não é seguro para threads, não vejo isso como um problema a ser resolvido tanto com testes de unidade / integração. O ideal seria restringir o acesso . Se o seu código da GUI estiver dissociado da lógica de negócios, você poderá aplicar um design que restrinja o acesso a essas chamadas por qualquer coisa que não seja o encadeamento / objeto que o cria *. Esse modo distante ideal para mim é tornar impossível para outros threads chamar essas funções do que tentar garantir que não.

  • Sim, percebo que sempre há maneiras de contornar as restrições de design que você impõe normalmente, onde o compilador não pode protegê-lo. Estou apenas falando praticamente; se você pode abstrair o objeto "Thread da GUI" ou qualquer outra coisa, pode ser o único que entregou um parâmetro aos objetos / funções da GUI e poderá restringir esse objeto de ter acesso a outros threads. É claro que pode ser capaz de contornar e cavar fundo e contornar esses aros para passar as funções / objetos da GUI para outros segmentos a serem invocados, mas pelo menos há uma barreira lá, e você pode chamar qualquer pessoa que faça isso de "idiota" ", e não estar errado, pelo menos, por ignorar claramente e procurar brechas para o que o design obviamente estava tentando restringir. :-D Isso '
Dragon Energy
fonte