Como devo testar o código encadeado da unidade?


704

Até agora, evitei o pesadelo que está testando o código multiencadeado, já que parece um campo minado demais. Gostaria de perguntar como as pessoas testaram código que depende de threads para execução bem-sucedida, ou como as pessoas testaram esses tipos de problemas que só aparecem quando dois threads interagem de uma determinada maneira?

Isso parece ser um problema realmente importante para os programadores hoje em dia, seria útil reunir nosso conhecimento sobre esse assunto.


2
Eu estava pensando em postar uma pergunta exatamente sobre o mesmo problema. Embora Will faça muitos pontos positivos abaixo, acho que podemos fazer melhor. Concordo que não existe uma "abordagem" única para lidar com isso de maneira limpa. No entanto, "testar da melhor maneira possível" está definindo um nível muito baixo. Voltarei com minhas descobertas.
Zach Burlingame

Em Java: O pacote java.util.concurrent contém algumas classes conhecidas, que podem ajudar a escrever testes JUnit determinísticos. Dê uma olhada em - CountDownLatch - Semaphore - Exchanger
Synox

Você pode fornecer um link para sua pergunta relacionada ao teste de unidade anterior, por favor?
Andrew Grimm


7
Eu acho importante notar que essa pergunta tem 8 anos e as bibliotecas de aplicativos já percorreram um longo caminho nesse meio tempo. Na "era moderna" (2016), o desenvolvimento multiencadeado surge principalmente em sistemas embarcados. Mas se você estiver trabalhando em um aplicativo para computador ou telefone, explore primeiro as alternativas. Os ambientes de aplicativos como o .NET agora incluem ferramentas para gerenciar ou simplificar muito provavelmente 90% dos cenários comuns de multithreading. (asnync / waiting, PLinq, IObservable, TPL ...). Código multiencadeado é difícil. Se você não reinventar a roda, não precisará testá-la novamente.
Paul Williams

Respostas:


245

Olha, não há uma maneira fácil de fazer isso. Estou trabalhando em um projeto que é inerentemente multithread. Os eventos vêm do sistema operacional e eu tenho que processá-los simultaneamente.

A maneira mais simples de lidar com o código de aplicativo complexo e multithread é: Se é muito complexo para testar, você está fazendo errado. Se você tiver uma única instância com vários encadeamentos atuando sobre ele e não puder testar situações em que esses encadeamentos se entrelaçam, seu design precisará ser refeito. É tão simples e tão complexo quanto isso.

Há muitas maneiras de programar para multithreading que evita threads executando através de instâncias ao mesmo tempo. O mais simples é tornar todos os seus objetos imutáveis. Claro, isso geralmente não é possível. Portanto, você precisa identificar os locais em seu design em que os threads interagem com a mesma instância e reduzir o número desses locais. Ao fazer isso, você isola algumas classes nas quais o multithreading realmente ocorre, reduzindo a complexidade geral de testar seu sistema.

Mas você precisa perceber que, mesmo fazendo isso, ainda não pode testar todas as situações em que dois threads se interagem. Para fazer isso, você teria que executar dois threads simultaneamente no mesmo teste e controlar exatamente quais linhas eles estão executando a qualquer momento. O melhor que você pode fazer é simular essa situação. Mas isso pode exigir que você codifique especificamente para testes, e isso é, na melhor das hipóteses, meio passo em direção a uma solução verdadeira.

Provavelmente, a melhor maneira de testar o código para problemas de encadeamento é através da análise estática do código. Se o seu código encadeado não seguir um conjunto finito de padrões seguros de encadeamento, você poderá ter um problema. Acredito que a Análise de código no VS contém algum conhecimento de encadeamento, mas provavelmente não muito.

Veja como as coisas estão atualmente (e provavelmente permanecerão por um bom tempo), a melhor maneira de testar aplicativos multithread é reduzir a complexidade do código encadeado o máximo possível. Minimize as áreas em que os threads interagem, teste da melhor maneira possível e use a análise de código para identificar áreas de perigo.


1
A análise de código é excelente se você lida com uma linguagem / estrutura que permita isso. EG: O Findbugs encontrará problemas de simultaneidade compartilhados muito simples e fáceis com variáveis ​​estáticas. O que ele não consegue encontrar são padrões de design únicos, pois assume que todos os objetos podem ser criados várias vezes. Este plugin é lamentavelmente inadequado para estruturas como o Spring.
Zombies

3
na verdade existe uma cura: objetos ativos. drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/...
Dill

6
Embora este seja um bom conselho, ainda estou perguntando: "como faço para testar as áreas mínimas em que vários threads são necessários?"
Bryan Rayner 27/16

5
"Se é muito complexo para testar, você está fazendo errado" - todos temos que mergulhar no código legado que não escrevemos. Como essa observação ajuda alguém exatamente?
Ronna 12/09

2
A análise estática é provavelmente uma boa ideia, mas não está testando. Este post realmente não responde à pergunta, que é sobre como testar.
perfil completo de Warren Dew

96

Já faz um tempo que essa pergunta foi publicada, mas ainda não foi respondida ...

A resposta de kleolb02 é boa. Vou tentar entrar em mais detalhes.

Existe uma maneira que eu pratico para código C #. Para testes de unidade, você deve poder programar testes reproduzíveis , que é o maior desafio no código multithread. Portanto, minha resposta tem como objetivo forçar o código assíncrono em um equipamento de teste, que funciona de forma síncrona .

É uma idéia do livro " xUnit Test Patterns " de Gerard Meszardos e é chamado "Objeto Humilde" (p. 695): É necessário separar o código lógico principal e qualquer coisa que cheira a código assíncrono. Isso resultaria em uma classe para a lógica principal, que funciona de forma síncrona .

Isso coloca você na posição de testar o código lógico principal de maneira síncrona . Você tem controle absoluto sobre o tempo das chamadas que está realizando na lógica principal e, portanto, pode fazer testes reproduzíveis . E esse é o seu ganho ao separar a lógica principal e a lógica assíncrona.

Essa lógica principal precisa ser contornada por outra classe, responsável por receber chamadas para a lógica principal de forma assíncrona e delegar essas chamadas na lógica principal. O código de produção acessará apenas a lógica principal por meio dessa classe. Como essa classe deve delegar apenas chamadas, é uma classe muito "burra" sem muita lógica. Portanto, você pode manter seus testes de unidade para esta classe trabalhadora assíncrona no mínimo.

Qualquer coisa acima disso (testando a interação entre classes) são testes de componentes. Também nesse caso, você deve ter controle absoluto sobre o tempo, se seguir o padrão "Objeto humilde".


1
Mas, às vezes, se os threads cooperam bem uns com os outros, algo também deve ser testado, certo? Definitivamente, separarei a lógica principal da parte assíncrona depois de ler sua resposta. Mas ainda vou testar a lógica por meio de interfaces assíncronas com um retorno de chamada "trabalhe em todos os segmentos".
CopperCash

E os sistemas com vários processadores?
21815 Technophile #

65

Difícil mesmo! Nos meus testes de unidade (C ++), dividi isso em várias categorias ao longo das linhas do padrão de simultaneidade usado:

  1. Testes de unidade para classes que operam em um único encadeamento e não têm conhecimento de encadeamento - fácil, teste como de costume.

  2. Testes de unidade para objetos Monitor (aqueles que executam métodos sincronizados no encadeamento de controle dos chamadores) que expõem uma API pública sincronizada - instanciam vários encadeamentos simulados que exercitam a API. Construa cenários que exercitam condições internas do objeto passivo. Inclua um teste em execução mais longo, que basicamente o supere a partir de vários threads por um longo período de tempo. Isso não é científico, eu sei, mas gera confiança.

  3. Testes de unidade para objetos ativos (aqueles que encapsulam seu próprio segmento ou segmentos de controle) - semelhante ao item 2 acima, com variações dependendo do design da classe. A API pública pode estar bloqueando ou não, os chamadores podem obter futuros, os dados podem chegar às filas ou precisam ser desenfileirados. Existem muitas combinações possíveis aqui; caixa branca de distância. Ainda requer vários threads simulados para fazer chamadas para o objeto em teste.

Como um aparte:

No treinamento interno para desenvolvedores que eu faço, ensino os Pilares de Concorrência e esses dois padrões como a estrutura principal para pensar e decompor problemas de concorrência. Obviamente, existem conceitos mais avançados por aí, mas descobri que esse conjunto básico ajuda a manter os engenheiros fora da sopa. Isso também leva a um código mais testável por unidade, conforme descrito acima.


51

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.


3
Os testes podem apenas revelar a presença de bugs, não a ausência deles. A pergunta original pergunta sobre um problema de 2 threads; nesse caso, testes exaustivos podem ser possíveis, mas geralmente não são. Para qualquer coisa além dos cenários mais simples, você pode ter que morder a bala e usar métodos formais - mas não pule os testes de unidade! A escrita correta do código multithread é difícil em primeiro lugar, mas um problema igualmente difícil é a prova de futuro contra regressão.
Paul Williams

4
Resumo surpreendente de uma das maneiras menos compreendidas. Sua resposta é a verdadeira segregação que as pessoas geralmente ignoram.
Prash

1
Uma dúzia de segundos é muito tempo, mesmo se você tiver apenas algumas centenas de testes de que o comprimento ...
Toby Speight

1
@TobySpeight Os testes são longos em comparação com os testes unitários normais. Descobri que meia dúzia de testes é mais que suficiente se o código encadeado for adequadamente projetado para ser o mais simples possível - porém, precisar de algumas centenas de testes multithreading quase certamente indicaria um arranjo de encadeamento excessivamente complexo.
Warren Orvalho

2
Esse é um bom argumento para manter a lógica do thread o mais separável da funcionalidade possível (eu sei, muito mais fácil falar do que fazer). E, se possível, dividir o conjunto de testes em conjuntos "todas as alterações" e "pré-confirmação" (para que seus testes de minuto a minuto não sejam afetados demais).
Toby Speight

22

Eu também tive sérios problemas ao testar código multiencadeado. Então eu encontrei uma solução muito legal em "xUnit Test Patterns", de Gerard Meszaros. O padrão que ele descreve é ​​chamado de objeto humilde .

Basicamente, descreve como você pode extrair a lógica em um componente separado e fácil de testar, que é separado do ambiente. Depois de testar essa lógica, você pode testar o comportamento complicado (multiencadeamento, execução assíncrona, etc ...)


20

Existem algumas ferramentas que são bastante boas. Aqui está um resumo de alguns dos Java.

Algumas boas ferramentas de análise estática incluem FindBugs (fornece algumas dicas úteis), JLint , Java Pathfinder (JPF e JPF2) e Bogor .

MultithreadedTC é uma boa ferramenta de análise dinâmica (integrada ao JUnit), na qual é necessário configurar seus próprios casos de teste.

O ConTest da IBM Research é interessante. Ele instrumenta seu código inserindo todos os tipos de comportamentos de modificação de threads (por exemplo, sono e rendimento) para tentar descobrir erros aleatoriamente.

GIRAR é uma ferramenta muito legal para modelar seus componentes Java (e outros), mas você precisa ter uma estrutura útil. É difícil de usar como está, mas extremamente poderoso se você souber usá-lo. Algumas ferramentas usam SPIN embaixo do capô.

O MultithreadedTC é provavelmente o mais popular, mas definitivamente vale a pena examinar algumas das ferramentas de análise estática acima.


16

A disponibilidade também pode ser útil para ajudá-lo a escrever testes de unidade determinísticos. Ele permite que você aguarde até que algum estado em algum lugar do seu sistema seja atualizado. Por exemplo:

await().untilCall( to(myService).myMethod(), greaterThan(3) );

ou

await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

Ele também tem suporte para Scala e Groovy.

await until { something() > 4 } // Scala example

1
Awaitility é brilhante - exatamente o que eu estava procurando!
Forge_7

14

Outra maneira de testar códigos encadeados e sistemas muito complexos em geral é através do Fuzz Testing . Não é ótimo e não encontrará tudo, mas é provável que seja útil e simples de fazer.

Citar:

Teste de fuzz ou fuzzing é uma técnica de teste de software que fornece dados aleatórios ("fuzz") para as entradas de um programa. Se o programa falhar (por exemplo, travando ou com falha nas asserções de código internas), os defeitos poderão ser observados. A grande vantagem do teste de fuzz é que o design do teste é extremamente simples e livre de preconceitos sobre o comportamento do sistema.

...

O teste de fuzz é freqüentemente usado em grandes projetos de desenvolvimento de software que empregam testes de caixa preta. Esses projetos geralmente têm um orçamento para desenvolver ferramentas de teste, e o teste de fuzz é uma das técnicas que oferece um alto custo-benefício.

...

No entanto, o teste de fuzz não substitui o teste exaustivo ou métodos formais: ele pode fornecer apenas uma amostra aleatória do comportamento do sistema e, em muitos casos, passar por um teste de fuzz pode demonstrar apenas que um software lida com exceções sem travar, em vez de comportando-se corretamente. Portanto, o teste de fuzz pode ser considerado apenas uma ferramenta de busca de bugs e não uma garantia de qualidade.


13

Eu fiz muito disso, e sim, é péssimo.

Algumas dicas:

  • GroboUtils para executar vários threads de teste
  • alphaWorks ConTest para classes de instrumentos para fazer com que as intercalações variem entre as iterações
  • Crie um throwablecampo e faça check-in tearDown(consulte a Listagem 1). Se você capturar uma exceção ruim em outro encadeamento, atribua-a ao throwable.
  • Criei a classe utils na Listagem 2 e a achei inestimável, especialmente waitForVerify e waitForCondition, o que aumentará bastante o desempenho de seus testes.
  • Faça bom uso AtomicBooleanem seus testes. É seguro para threads, e muitas vezes você precisará de um tipo de referência final para armazenar valores de classes de retorno de chamada e afins. Veja o exemplo na Listagem 3.
  • Sempre dê um tempo limite ao seu teste (por exemplo, @Test(timeout=60*1000)), pois os testes de simultaneidade às vezes podem travar para sempre quando são interrompidos.

Listagem 1:

@After
public void tearDown() {
    if ( throwable != null )
        throw throwable;
}

Listagem 2:

import static org.junit.Assert.fail;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Random;
import org.apache.commons.collections.Closure;
import org.apache.commons.collections.Predicate;
import org.apache.commons.lang.time.StopWatch;
import org.easymock.EasyMock;
import org.easymock.classextension.internal.ClassExtensionHelper;
import static org.easymock.classextension.EasyMock.*;

import ca.digitalrapids.io.DRFileUtils;

/**
 * Various utilities for testing
 */
public abstract class DRTestUtils
{
    static private Random random = new Random();

/** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with
 * default max wait and check period values.
 */
static public void waitForCondition(Predicate predicate, String errorMessage) 
    throws Throwable
{
    waitForCondition(null, null, predicate, errorMessage);
}

/** Blocks until a condition is true, throwing an {@link AssertionError} if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param errorMessage message use in the {@link AssertionError}
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, String errorMessage) throws Throwable 
{
    waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() {
        public void execute(Object errorMessage)
        {
            fail((String)errorMessage);
        }
    }, errorMessage);
}

/** Blocks until a condition is true, running a closure if
 * it does not become true during a given max time.
 * @param maxWait_ms max time to wait for true condition. Optional; defaults
 * to 30 * 1000 ms (30 seconds).
 * @param checkPeriod_ms period at which to try the condition. Optional; defaults
 * to 100 ms.
 * @param predicate the condition
 * @param closure closure to run
 * @param argument argument for closure
 * @throws Throwable on {@link AssertionError} or any other exception/error
 */
static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, 
    Predicate predicate, Closure closure, Object argument) throws Throwable 
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    if ( checkPeriod_ms == null )
        checkPeriod_ms = 100;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    while ( !predicate.evaluate(null) ) {
        Thread.sleep(checkPeriod_ms);
        if ( stopWatch.getTime() > maxWait_ms ) {
            closure.execute(argument);
        }
    }
}

/** Calls {@link #waitForVerify(Integer, Object)} with <code>null</code>
 * for {@code maxWait_ms}
 */
static public void waitForVerify(Object easyMockProxy)
    throws Throwable
{
    waitForVerify(null, easyMockProxy);
}

/** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a
 * max wait time has elapsed.
 * @param maxWait_ms Max wait time. <code>null</code> defaults to 30s.
 * @param easyMockProxy Proxy to call verify on
 * @throws Throwable
 */
static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy)
    throws Throwable
{
    if ( maxWait_ms == null )
        maxWait_ms = 30 * 1000;
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for(;;) {
        try
        {
            verify(easyMockProxy);
            break;
        }
        catch (AssertionError e)
        {
            if ( stopWatch.getTime() > maxWait_ms )
                throw e;
            Thread.sleep(100);
        }
    }
}

/** Returns a path to a directory in the temp dir with the name of the given
 * class. This is useful for temporary test files.
 * @param aClass test class for which to create dir
 * @return the path
 */
static public String getTestDirPathForTestClass(Object object) 
{

    String filename = object instanceof Class ? 
        ((Class)object).getName() :
        object.getClass().getName();
    return DRFileUtils.getTempDir() + File.separator + 
        filename;
}

static public byte[] createRandomByteArray(int bytesLength)
{
    byte[] sourceBytes = new byte[bytesLength];
    random.nextBytes(sourceBytes);
    return sourceBytes;
}

/** Returns <code>true</code> if the given object is an EasyMock mock object 
 */
static public boolean isEasyMockMock(Object object) {
    try {
        InvocationHandler invocationHandler = Proxy
                .getInvocationHandler(object);
        return invocationHandler.getClass().getName().contains("easymock");
    } catch (IllegalArgumentException e) {
        return false;
    }
}
}

Listagem 3:

@Test
public void testSomething() {
    final AtomicBoolean called = new AtomicBoolean(false);
    subject.setCallback(new SomeCallback() {
        public void callback(Object arg) {
            // check arg here
            called.set(true);
        }
    });
    subject.run();
    assertTrue(called.get());
}

2
Um tempo limite é uma boa idéia, mas se um teste atingir o tempo limite, quaisquer resultados posteriores nessa execução serão suspeitos. O teste de tempo limite ainda pode ter alguns threads em execução que podem atrapalhar você.
Don Kirkby

12

Testar o código MT para correção é, como já foi dito, um problema bastante difícil. No final, tudo se resume a garantir que não haja corridas de dados sincronizadas incorretamente no seu código. O problema disso é que existem infinitas possibilidades de execução de encadeamentos (intercalações) sobre as quais você não tem muito controle (leia este artigo). Em cenários simples, pode ser possível realmente provar a correção pelo raciocínio, mas esse geralmente não é o caso. Especialmente se você deseja evitar / minimizar a sincronização e não optar pela opção de sincronização mais óbvia / mais fácil.

Uma abordagem que eu sigo é escrever código de teste altamente simultâneo para tornar possível a ocorrência de corridas de dados potencialmente não detectadas. E então eu executo esses testes por algum tempo :) Certa vez, deparei-me com uma palestra em que algum cientista da computação exibia uma ferramenta que faz isso (projetar aleatoriamente testes de especificações e depois executá-los descontroladamente, simultaneamente, verificando os invariantes definidos ser quebrado).

A propósito, acho que esse aspecto do teste do código MT não foi mencionado aqui: identifique invariantes do código que você pode verificar aleatoriamente. Infelizmente, encontrar esses invariantes também é um problema bastante difícil. Além disso, eles podem não demorar o tempo todo durante a execução; portanto, você precisa encontrar / aplicar pontos de execução em que possa esperar que sejam verdadeiros. Trazer a execução do código a esse estado também é um problema difícil (e pode, por si só, gerar problemas de simultaneidade. Ufa, é muito difícil!

Alguns links interessantes para ler:


autor refere-se à randomização nos testes. Pode ser o QuickCheck , que foi portado para vários idiomas. Você pode assistir a palestra sobre tais testes para o sistema concorrente aqui
Max

6

Pete Goodliffe tem uma série de testes unitários de código encadeado .

É difícil. Pego a saída mais fácil e tento manter o código de segmentação abstraído do teste real. Pete menciona que o jeito que eu faço é errado, mas eu entendi a separação ou tive sorte.


6
Li os dois artigos publicados até agora e não os achei muito úteis. Ele apenas fala sobre as dificuldades sem dar muitos conselhos concretos. Talvez artigos futuros melhorem.
Don Kirkby

6

Para Java, consulte o capítulo 12 do JCIP . Existem alguns exemplos concretos de escrever testes de unidade determinísticos e multithread para pelo menos testar a correção e os invariantes do código simultâneo.

"Provar" a segurança da rosca com testes de unidade é muito mais complicado. Minha opinião é que isso é melhor atendido por testes de integração automatizados em uma variedade de plataformas / configurações.


6

Eu gosto de escrever dois ou mais métodos de teste para executar em threads paralelos, e cada um deles faz chamadas para o objeto em teste. Eu tenho usado chamadas Sleep () para coordenar a ordem das chamadas dos diferentes segmentos, mas isso não é realmente confiável. Também é muito mais lento, porque você precisa dormir o suficiente para que o tempo normalmente funcione.

Encontrei a biblioteca Java Multithreaded TC do mesmo grupo que escreveu o FindBugs. Ele permite que você especifique a ordem dos eventos sem usar o Sleep (), e é confiável. Ainda não tentei.

A maior limitação dessa abordagem é que ela só permite testar os cenários que você suspeita que causem problemas. Como outros já disseram, você realmente precisa isolar seu código multithread em um pequeno número de classes simples para ter alguma esperança de testá-los completamente.

Depois de testar cuidadosamente os cenários que você espera causar problemas, um teste não científico que lança um monte de solicitações simultâneas na classe por um tempo é uma boa maneira de procurar problemas inesperados.

Atualização: joguei um pouco com a biblioteca Java Multithreaded TC, e funciona bem. Também portamos alguns de seus recursos para uma versão .NET que chamo de TickingTest .


5

Lido com testes de unidade de componentes rosqueados da mesma maneira que lida com qualquer teste de unidade, ou seja, com inversão de estruturas de controle e isolamento. Eu desenvolvo na arena .Net e fora da caixa o enfiamento (entre outras coisas) é muito difícil (eu diria quase impossível) isolar completamente.

Portanto, eu escrevi wrappers que se parecem com isso (simplificado):

public interface IThread
{
    void Start();
    ...
}

public class ThreadWrapper : IThread
{
    private readonly Thread _thread;

    public ThreadWrapper(ThreadStart threadStart)
    {
        _thread = new Thread(threadStart);
    }

    public Start()
    {
        _thread.Start();
    }
}

public interface IThreadingManager
{
    IThread CreateThread(ThreadStart threadStart);
}

public class ThreadingManager : IThreadingManager
{
    public IThread CreateThread(ThreadStart threadStart)
    {
         return new ThreadWrapper(threadStart)
    }
}

A partir daí, posso injetar facilmente o IThreadingManager nos meus componentes e usar minha estrutura de isolamento de escolha para fazer com que o thread se comporte conforme o esperado durante o teste.

Até agora, isso funcionou muito bem para mim e eu uso a mesma abordagem para o pool de threads, as coisas no System.Environment, Sleep etc. etc.


5

Dê uma olhada na minha resposta relacionada em

Projetando uma classe de teste para uma barreira personalizada

É tendencioso em relação ao Java, mas possui um resumo razoável das opções.

Em resumo, porém (IMO) não é o uso de alguma estrutura sofisticada que garanta a correção, mas como você cria o código multithread. Dividir as preocupações (simultaneidade e funcionalidade) é uma maneira enorme de aumentar a confiança.O software orientado a objetos em crescimento, guiado por testes, explica algumas opções melhor do que eu.

Análise estática e métodos formais (ver, Concorrência: modelos de estado e programas Java ) é uma opção, mas eu os achei de uso limitado no desenvolvimento comercial.

Não esqueça que raramente são garantidos quaisquer testes de estilo de carga / imersão para destacar problemas.

Boa sorte!


Você também deve mencionar sua tempus-fugitbiblioteca aqui, que helps write and test concurrent code;))
Idolon 17/09/13

4

Eu descobri recentemente (para Java) uma ferramenta chamada Threadsafe. É uma ferramenta de análise estática muito parecida com o findbugs, mas especificamente para detectar problemas com vários threads. Não é um substituto para o teste, mas eu posso recomendá-lo como parte da criação de Java confiável com vários threads.

Ele ainda captura alguns problemas potenciais muito sutis em torno de coisas como subsunção de classe, acesso a objetos não seguros por meio de classes simultâneas e detecção de modificadores voláteis ausentes ao usar o paradigma de bloqueio verificado duas vezes.

Se você escreve Java multithread , experimente.


3

O artigo a seguir sugere 2 soluções. Quebrando um semáforo (CountDownLatch) e adicionando funcionalidades como externalizar dados do encadeamento interno. Outra maneira de atingir esse objetivo é usar o Pool de Threads (consulte Pontos de Interesse).

Aspersor - Objeto de sincronização avançado


3
Por favor, explique as abordagens aqui, links externos podem estar mortos no futuro.
Uooo

2

Passei a maior parte da semana passada em uma biblioteca da universidade estudando a depuração de código simultâneo. O problema central é que o código simultâneo não é determinístico. Normalmente, a depuração acadêmica caiu em um dos três campos aqui:

  1. Rastreamento / repetição de evento. Isso requer um monitor de eventos e, em seguida, a revisão dos eventos que foram enviados. Em uma estrutura UT, isso envolveria o envio manual dos eventos como parte de um teste e, em seguida, a revisão post-mortem.
  2. Scriptable. É aqui que você interage com o código em execução com um conjunto de gatilhos. "Em x> foo, baz ()". Isso pode ser interpretado em uma estrutura UT, na qual você tem um sistema de tempo de execução acionando um determinado teste em uma determinada condição.
  3. Interativo. Obviamente, isso não funcionará em uma situação de teste automático. ;)

Agora, como observaram os comentaristas acima, você pode projetar seu sistema simultâneo em um estado mais determinístico. No entanto, se você não fizer isso corretamente, retornará ao projeto de um sistema seqüencial novamente.

Minha sugestão seria focar em ter um protocolo de design muito rigoroso sobre o que é encadeado e o que não é encadeado. Se você restringir sua interface para que haja dependências mínimas entre os elementos, é muito mais fácil.

Boa sorte e continue trabalhando no problema.


2

Eu tive a infeliz tarefa de testar código encadeado e eles são definitivamente os testes mais difíceis que já escrevi.

Ao escrever meus testes, usei uma combinação de delegados e eventos. Basicamente, trata-se de usar PropertyNotifyChangedeventos com um WaitCallbackou algum tipo de ConditionalWaiterpesquisa.

Não tenho certeza se essa foi a melhor abordagem, mas funcionou para mim.


1

Assumindo sob o código "multiencadeado", significou algo que é

  • com estado e mutável
  • AND acessado / modificado por vários threads simultaneamente

Em outras palavras, estamos falando sobre o teste de classe / método / unidade segura e segura de threads com estado - que deve ser uma fera muito rara atualmente.

Como esse animal é raro, precisamos primeiro garantir que haja desculpas válidas para escrevê-lo.

Etapa 1. Considere modificar o estado no mesmo contexto de sincronização.

Hoje, é fácil escrever código simultâneo e assíncrono com capacidade de composição, onde as E / S ou outras operações lentas são transferidas para segundo plano, mas o estado compartilhado é atualizado e consultado em um contexto de sincronização. por exemplo, tarefas assíncronas / aguardadas e Rx no .NET etc. - todas elas podem ser testadas por design, tarefas "reais" e agendadores podem ser substituídos para tornar o teste determinístico (no entanto, isso está fora do escopo da questão).

Pode parecer muito restrito, mas essa abordagem funciona surpreendentemente bem. É possível escrever aplicativos inteiros nesse estilo sem precisar tornar nenhum estado seguro para threads (eu faço).

Etapa 2. Se a manipulação do estado compartilhado no contexto de sincronização única não for absolutamente possível.

Verifique se a roda não está sendo reinventada / definitivamente não há alternativa padrão que possa ser adaptada para o trabalho. Deve ser provável que o código seja muito coeso e contido em uma unidade, por exemplo, com uma boa chance, é um caso especial de alguma estrutura de dados padrão segura para threads, como mapa ou coleção de hash ou qualquer outra coisa.

Nota: se o código for grande / se estender por várias classes E precisar de manipulação de estado de vários segmentos, há uma chance muito grande de que o design não seja bom, reconsidere a Etapa 1

Etapa 3. Se essa etapa for alcançada, precisamos testar nossa própria classe / método / unidade segura e segura de thread com estado .

Serei sincero: nunca tive que escrever testes adequados para esse código. Na maioria das vezes, passo na Etapa 1, às vezes na Etapa 2. A última vez que tive que escrever um código de segurança de thread personalizado foi há tantos anos atrás, antes de eu adotar o teste de unidade / provavelmente não precisaria escrevê-lo com o conhecimento atual de qualquer maneira.

Se eu realmente tivesse que testar esse código ( finalmente, resposta real ), tentaria algumas coisas abaixo

  1. Teste de estresse não determinístico. por exemplo, execute 100 threads simultaneamente e verifique se o resultado final é consistente. Isso é mais típico para testes de nível superior / integração de cenários de vários usuários, mas também pode ser usado no nível da unidade.

  2. Exponha alguns ganchos de teste nos quais o teste pode injetar algum código para ajudar a criar cenários determinísticos nos quais um encadeamento deve executar a operação antes do outro. Por mais feio que seja, não consigo pensar em nada melhor.

  3. Teste controlado por atraso para fazer com que os threads sejam executados e executem operações em ordem específica. Estritamente falando, esses testes também não são determinísticos (há uma chance de a coleção de GC congelar / parar o mundo do sistema, que pode distorcer atrasos orquestrados), também é feio, mas permite evitar ganchos.


0

Para o código J2E, usei o SilkPerformer, LoadRunner e JMeter para testes de simultaneidade de threads. Todos eles fazem a mesma coisa. Basicamente, eles fornecem uma interface relativamente simples para administrar sua versão do servidor proxy, necessária para analisar o fluxo de dados TCP / IP e simular vários usuários fazendo solicitações simultâneas ao servidor de aplicativos. O servidor proxy pode permitir que você analise as solicitações feitas, apresentando a página inteira e o URL enviados ao servidor, bem como a resposta do servidor, após o processamento da solicitação.

Você pode encontrar alguns erros no modo http inseguro, onde você pode pelo menos analisar os dados do formulário que estão sendo enviados e alterá-los sistematicamente para cada usuário. Mas os verdadeiros testes são quando você executa em https (Secured Socket Layers). Além disso, você também precisa alterar sistematicamente os dados da sessão e dos cookies, o que pode ser um pouco mais complicado.

O melhor bug que eu já encontrei, enquanto testava a simultaneidade, foi quando descobri que o desenvolvedor confiava na coleta de lixo Java para fechar a solicitação de conexão estabelecida no logon no servidor LDAP ao efetuar login. Isso resultou na exposição dos usuários. para sessões de outros usuários e resultados muito confusos, ao tentar analisar o que aconteceu quando o servidor foi ajoelhado, mal conseguindo concluir uma transação a cada poucos segundos.

No final, você ou alguém provavelmente terá que ceder e analisar o código de erros como o que acabei de mencionar. E uma discussão aberta entre departamentos, como a que ocorreu, quando revelamos o problema descrito acima, é mais útil. Mas essas ferramentas são a melhor solução para testar código multiencadeado. O JMeter é de código aberto. O SilkPerformer e o LoadRunner são proprietários. Se você realmente quer saber se seu aplicativo é seguro para threads, é assim que os garotos grandes fazem isso. Fiz isso profissionalmente para empresas muito grandes, por isso não acho. Estou falando por experiência pessoal.

Uma palavra de cautela: leva algum tempo para entender essas ferramentas. Não será apenas uma questão de instalar o software e ativar a GUI, a menos que você já tenha tido alguma exposição à programação multithread. Tentei identificar as três categorias críticas de áreas a serem compreendidas (formulários, dados de sessão e cookie), com a esperança de que pelo menos começar a entender esses tópicos o ajude a se concentrar em resultados rápidos, em vez de precisar ler as documentação inteira.


0

A simultaneidade é uma interação complexa entre o modelo de memória, hardware, caches e nosso código. No caso de Java, pelo menos esses testes foram parcialmente abordados principalmente pelo jcstress . Os criadores dessa biblioteca são conhecidos por serem autores de muitos recursos de simultaneidade JVM, GC e Java.

Mas mesmo essa biblioteca precisa de um bom conhecimento da especificação do Java Memory Model para que saibamos exatamente o que estamos testando. Mas acho que o foco desse esforço são marcas de mircobench. Não são aplicativos de negócios enormes.


0

Há um artigo sobre o tópico, usando Rust como o idioma no código de exemplo:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

Em resumo, o truque é escrever sua lógica simultânea para que seja robusta ao não-determinismo envolvido em vários encadeamentos de execução, usando ferramentas como canais e condvars.

Então, se foi assim que você estruturou seus "componentes", a maneira mais fácil de testá-los é usando canais para enviar mensagens a eles e, em seguida, bloqueie em outros canais para afirmar que o componente envia certas mensagens esperadas.

O artigo vinculado é totalmente escrito usando testes de unidade.


-1

Se você estiver testando um novo e simples Thread (executável) .run (), você pode simular o Thread para executar o executável sequencialmente

Por exemplo, se o código do objeto testado chamar um novo thread como este

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

Zombar de novos Threads e executar o argumento executável sequencialmente pode ajudar

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if(runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

-3

(se possível) não use threads, use atores / objetos ativos. Fácil de testar.


2
@OMTheEternity talvez, mas ainda é a melhor resposta.
Dill

-5

Você pode usar EasyMock.makeThreadSafe para tornar a instância de teste threadsafe


Essa não é a maneira mais possível de testar o código multithread. O problema não é que o código de teste execute multiencadeamentos, mas que você teste o código que normalmente executa multiencadeamentos. E você não pode sincronizar tudo, porque na verdade você não testa mais as corridas de dados.
bennidi
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.