Eu enfrentei esse problema várias vezes nos últimos anos ao escrever código de manipulação de threads para vários projetos. Estou fornecendo uma resposta tardia porque a maioria das outras respostas, embora ofereça alternativas, na verdade não responde à pergunta sobre o teste. Minha resposta é dirigida aos casos em que não há alternativa ao código multithread; Abordo os problemas de design de código para garantir a integridade, mas também discuto o teste de unidade.
Escrevendo código multithread testável
A primeira coisa a fazer é separar o código de manipulação do encadeamento de produção de todo o código que efetua o processamento de dados. Dessa forma, o processamento de dados pode ser testado como código de thread único, e a única coisa que o código multithread faz é coordenar os threads.
A segunda coisa a lembrar é que os erros no código multithread são probabilísticos; os erros que se manifestam com menos frequência são os erros que entrarão furtivamente na produção, serão difíceis de reproduzir mesmo na produção e, portanto, causarão os maiores problemas. Por esse motivo, a abordagem de codificação padrão de escrever o código rapidamente e depois depurá-lo até que funcione é uma má idéia para o código multithread; resultará em código onde os bugs fáceis são corrigidos e os bugs perigosos ainda estão lá.
Em vez disso, ao escrever código multithread, você deve escrever o código com a atitude de evitar os erros em primeiro lugar. Se você removeu corretamente o código de processamento de dados, o código de manipulação de encadeamentos deve ser pequeno o suficiente - de preferência algumas linhas, no máximo algumas dúzias de linhas - para que você possa escrevê-lo sem escrever um bug e certamente sem escrever muitos bugs , se você entender a segmentação, não se apresse e tome cuidado.
Escrevendo testes de unidade para código multithread
Depois que o código multithread é escrito com o maior cuidado possível, ainda vale a pena escrever testes para esse código. O objetivo principal dos testes não é tanto testar bugs de condição de corrida altamente dependentes do tempo - é impossível testar repetidamente essas condições de corrida - mas sim testar se sua estratégia de bloqueio para impedir esses bugs permite que vários threads interajam como pretendido .
Para testar corretamente o comportamento correto de bloqueio, um teste deve iniciar vários threads. Para tornar o teste repetitivo, queremos que as interações entre os encadeamentos ocorram em uma ordem previsível. Não queremos sincronizar externamente os encadeamentos no teste, porque isso ocultará os erros que podem ocorrer na produção, onde os encadeamentos não são sincronizados externamente. Isso deixa o uso de atrasos de tempo na sincronização de threads, que é a técnica que utilizei com sucesso sempre que tive que escrever testes de código multithread.
Se os atrasos forem muito curtos, o teste se tornará frágil, porque pequenas diferenças de tempo - digamos entre máquinas diferentes nas quais os testes podem ser executados - podem causar o tempo desligado e o teste falhar. O que eu normalmente faço é começar com atrasos que causam falhas no teste, aumentar os atrasos para que o teste passe com confiabilidade na minha máquina de desenvolvimento e, em seguida, duplicar os atrasos além disso, para que o teste tenha uma boa chance de passar em outras máquinas. Isso significa que o teste levará um tempo macroscópico, embora, na minha experiência, o design cuidadoso do teste possa limitar esse tempo a não mais de uma dúzia de segundos. Como você não deve ter muitos locais que exigem código de coordenação de encadeamentos no seu aplicativo, isso deve ser aceitável para o seu conjunto de testes.
Por fim, acompanhe o número de bugs detectados pelo seu teste. Se o seu teste tiver 80% de cobertura de código, pode-se esperar que ocorra cerca de 80% dos seus erros. Se seu teste for bem projetado, mas não encontrar bugs, há uma chance razoável de que você não tenha bugs adicionais que aparecerão apenas na produção. Se o teste detectar um ou dois erros, você ainda poderá ter sorte. Além disso, você pode considerar uma revisão cuidadosa ou mesmo uma reescrita completa do seu código de manipulação de threads, pois é provável que o código ainda contenha bugs ocultos que serão muito difíceis de encontrar até que o código esteja em produção e muito difícil de corrigir então.