Deve ser “Arrange-Assert-Act-Assert”?

94

Em relação ao padrão de teste clássico de Arrange-Act-Assert , frequentemente me pego adicionando uma contra-afirmação que antecede a Act. Dessa forma, eu sei que a afirmação que passa está realmente passando como resultado da ação.

Eu penso nisso como análogo ao vermelho em red-green-refactor, onde somente se eu tiver visto a barra vermelha no decorrer do meu teste, eu sei que a barra verde significa que escrevi um código que faz a diferença. Se eu escrever um teste de aprovação, qualquer código o satisfará; da mesma forma, com relação a Arrange-Assert-Act-Assert, se minha primeira afirmação falhar, eu sei que qualquer ato teria sido aprovado na Assert final - de modo que não estava realmente verificando nada sobre a lei.

Seus testes seguem esse padrão? Por que ou por que não?

Esclarecimento da atualização : a afirmação inicial é essencialmente o oposto da afirmação final. Não é uma afirmação de que Arrange funcionou; é uma afirmação de que Act ainda não funcionou.

Carl Manaster
fonte

Respostas:

121

Esta não é a coisa mais comum de se fazer, mas ainda é comum o suficiente para ter seu próprio nome. Essa técnica é chamada de Asserção de Guarda . Você pode encontrar uma descrição detalhada dele na página 490 no excelente livro xUnit Test Patterns de Gerard Meszaros (altamente recomendado).

Normalmente, eu não uso esse padrão, pois acho mais correto escrever um teste específico que valide qualquer pré-condição que eu sinta necessidade de garantir. Esse teste sempre deve falhar se a pré-condição falhar, e isso significa que não preciso dele incorporado em todos os outros testes. Isso fornece um melhor isolamento de preocupações, já que um caso de teste verifica apenas uma coisa.

Pode haver muitas pré-condições que precisam ser satisfeitas para um determinado caso de teste, então você pode precisar de mais de uma Asserção do Guard. Em vez de repeti-los em todos os testes, ter um (e apenas um) teste para cada pré-condição mantém seu código de teste mais sustentável, já que você terá menos repetição dessa forma.

Mark Seemann
fonte
1, resposta muito boa. A última parte é especialmente importante, porque mostra que você pode proteger as coisas como um teste de unidade separado.
murrekatt de
3
Eu geralmente faço isso também, mas há um problema em ter um teste separado para garantir as pré-condições (especialmente com uma grande base de código com requisitos variáveis) - o teste de pré-condição será modificado com o tempo e sairá de sincronia com o 'principal' teste que pressupõe essas pré-condições. Portanto, as pré-condições podem estar todas boas e verdes, mas essas pré-condições não são satisfeitas no teste principal, que agora mostra sempre verde e bom. Mas se as pré-condições estivessem no teste principal, eles teriam falhado. Você já se deparou com esse problema e encontrou uma boa solução para ele?
nchaud
2
Se você mudar muito seus testes, poderá ter outros problemas , porque isso tenderá a tornar seus testes menos confiáveis. Mesmo em face das mudanças nos requisitos, considere projetar o código apenas com anexos .
Mark Seemann
@MarkSeemann Você está certo, que temos que minimizar a repetição, mas por outro lado pode haver uma série de coisas que podem impactar o Arrange para o teste específico, embora o próprio teste do Arrange passasse. Por exemplo, a limpeza para o teste de arranjo ou depois que outro teste estava com defeito e o arranjo não seria o mesmo que no teste de arranjo.
Rekshino
32

Também pode ser especificado como Arrange- Assume -Act -Assert.

Há um identificador técnico para isso no NUnit, como no exemplo aqui: http://nunit.org/index.php?p=theory&r=2.5.7

Ole Lynge
fonte
1
Agradável! Eu gosto de um quarto - e diferente - e preciso - "A". Obrigado!
Carl Manaster
+1, @Ole! Também gosto deste para alguns casos especiais! Vou dar uma chance!
John Tobler de
8

Aqui está um exemplo.

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
    range.encompass(7);
    assertTrue(range.includes(7));
}

Pode ser que eu tenha escrito Range.includes()simplesmente para retornar verdadeiro. Não o fiz, mas posso imaginar que sim. Ou eu poderia ter escrito errado de várias outras maneiras. Eu esperava e esperava que com TDD eu realmente acertasse - isso includes()simplesmente funciona - mas talvez não. Portanto, a primeira afirmação é uma verificação de sanidade, para garantir que a segunda afirmação seja realmente significativa.

Lido por si mesmo, assertTrue(range.includes(7));está dizendo: "afirmar que o intervalo modificado inclui 7". Lido no contexto da primeira asserção, está dizendo: "afirme que invocar encompass () faz com que inclua 7. E uma vez que engloba é a unidade que estamos testando, acho que tem algum valor (pequeno).

Estou aceitando minha própria resposta; muitos outros interpretaram mal minha pergunta como sendo sobre testar a configuração. Eu acho que isso é um pouco diferente.

Carl Manaster
fonte
Obrigado por voltar com um exemplo, Carl. Bem, na parte vermelha do ciclo TDD, até que o encompass () realmente faça algo; a primeira afirmação é inútil, é apenas uma duplicação da segunda. No verde, começa a ser útil. Está fazendo sentido durante a refatoração. Seria bom ter um framework UT que faça isso automaticamente.
filante
Suponha que você TDD essa classe Range, não haverá outro teste de falha testando o Range ctor, quando você quebrá-lo?
filante 01 de
1
@philippe: Não tenho certeza se entendi a pergunta. O construtor Range e includes () têm seus próprios testes de unidade. Você poderia explicar, por favor?
Carl Manaster
Para que a primeira asserção assertFalse (range.includes (7)) falhe, você precisa ter um defeito no Range Constructor. Então, eu quis perguntar se os testes para o construtor Range não serão interrompidos ao mesmo tempo que essa afirmação. E que tal afirmar após o ato em outro valor: por exemplo, assertFalse (range.includes (6))?
filante
1
A construção de alcance, a meu ver, vem antes de funções como includes (). Portanto, embora eu concorde, apenas um construtor defeituoso (ou um includes defeituoso ()) faria com que a primeira asserção falhasse, o teste do construtor não incluiria uma chamada para includes (). Sim, todas as funções até a primeira asserção já foram testadas. Mas essa afirmação negativa inicial está comunicando algo e, a meu ver, algo útil. Mesmo que cada afirmação seja aprovada quando for escrita inicialmente.
Carl Manaster,
7

Um Arrange-Assert-Act-Assertteste sempre pode ser refatorado em dois testes:

1. Arrange-Assert

e

2. Arrange-Act-Assert

O primeiro teste só fará valer o que foi configurado na fase de arranjo, e o segundo teste só valerá o que aconteceu na fase de ato.

Isso tem a vantagem de dar um feedback mais preciso sobre se foi a fase de arranjo ou ação que falhou, enquanto no original Arrange-Assert-Act-Asserteles estão combinados e você teria que cavar mais fundo e examinar exatamente qual afirmação falhou e por que falhou para saber se foi o arranjo ou ato que falhou.

Também satisfaz a intenção de testar melhor a unidade, pois você está separando o teste em unidades independentes menores.

Por último, tenha em mente que sempre que você vir seções similares de Arrange em diferentes testes, você deve tentar puxá-las para métodos auxiliares compartilhados, de modo que seus testes sejam mais DRY e mais fáceis de manter no futuro.

Sammi
fonte
3

Agora estou fazendo isso. AAAA de um tipo diferente

Arrange - setup
Act - what is being tested
Assemble - what is optionally needed to perform the assert
Assert - the actual assertions

Exemplo de um teste de atualização:

Arrange: 
    New object as NewObject
    Set properties of NewObject
    Save the NewObject
    Read the object as ReadObject

Act: 
    Change the ReadObject
    Save the ReadObject

Assemble: 
    Read the object as ReadUpdated

Assert: 
    Compare ReadUpdated with ReadObject properties

O motivo é que o ACT não contém a leitura do ReadUpdated é porque ele não faz parte do ato. O ato é apenas mudar e salvar. Então, realmente, ARRANGE ReadUpdated for assertion, estou chamando ASSEMBLE para assertion. Isso evita confundir a seção ARRANGE

ASSERT deve conter apenas asserções. Isso deixa ASSEMBLE entre ACT e ASSERT, que configura a declaração.

Por último, se você está falhando no Arrange, seus testes não estão corretos porque você deve ter outros testes para prevenir / encontrar esses bugs triviais . Porque para o cenário que apresento, já deve haver outros testes que testam READ e CREATE. Se você criar uma "Asserção de Guarda", pode estar interrompendo o DRY e criando manutenção.

Valamas
fonte
1

Lançar uma declaração de "verificação de integridade" para verificar o estado antes de executar a ação que está testando é uma técnica antiga. Eu geralmente os escrevo como um andaime de teste para provar a mim mesmo que o teste faz o que eu espero, e os removo mais tarde para evitar testes complicados com o andaime de teste. Às vezes, deixar o scaffolding ajuda o teste a servir de narrativa.

Dave W. Smith
fonte
1

Eu já li sobre essa técnica - possivelmente de você btw - mas não a uso; principalmente porque estou acostumado com o formulário triplo A para meus testes de unidade.

Agora, estou ficando curioso e tenho algumas perguntas: como você escreve seu teste, você faz com que essa afirmação falhe, seguindo um ciclo red-green-red-green-refactor, ou você adiciona depois?

Você falha às vezes, talvez depois de refatorar o código? O que isso diz a você ? Talvez você possa dar um exemplo em que ajudou. Obrigado.

filante
fonte
Eu normalmente não forço a afirmação inicial a falhar - afinal, ela não deve falhar, da forma como uma afirmação TDD deveria, antes de seu método ser escrito. Eu não escrevê-lo, quando eu escrevê-lo, antes , apenas no curso normal de escrever o teste, não depois. Honestamente, não consigo me lembrar de uma falha - talvez isso sugira que é uma perda de tempo. Vou tentar dar um exemplo, mas não tenho nenhum em mente no momento. Obrigado pelas perguntas; eles são úteis.
Carl Manaster de
1

Já fiz isso antes, ao investigar um teste que falhou.

Depois de coçar a cabeça consideravelmente, concluí que a causa eram os métodos chamados durante "Arrange" não estavam funcionando corretamente. A falha do teste foi enganosa. Eu adicionei um Assert após o arranjo. Isso fez com que o teste falhasse em um local que destacava o problema real.

Acho que também há um cheiro de código aqui se a parte Organizar do teste for muito longa e complicada.

WW.
fonte
Um ponto menor: eu consideraria o arranjo muito complicado mais como um cheiro de design do que um cheiro de código - às vezes, o design é tal que apenas um arranjo complicado permitirá que você teste a unidade. Menciono isso porque essa situação requer uma solução mais profunda do que um simples cheiro de código.
Carl Manaster
1

Em geral, gosto muito de "Organizar, agir, afirmar" e usar isso como meu padrão pessoal. A única coisa que não me lembra de fazer, no entanto, é desorganizar o que arranjei quando as afirmações forem feitas. Na maioria dos casos, isso não causa muito aborrecimento, já que a maioria das coisas desaparecem automaticamente por meio da coleta de lixo, etc. Se você estabeleceu conexões com recursos externos, no entanto, provavelmente desejará fechar essas conexões quando terminar com suas afirmações ou você tem um servidor ou recurso caro por aí em algum lugar segurando conexões ou recursos vitais que deveria ser capaz de ceder para outra pessoa. Isso é particularmente importante se você um daqueles desenvolvedores que não usa TearDown ou TestFixtureTearDownpara limpar após um ou mais testes. Claro, "Arrange, Act, Assert" não é responsável por minha falha em fechar o que abro; Eu só mencionei esse "pegadinha" porque ainda não encontrei um bom sinônimo de "palavra A" para "dispor" para recomendar! Alguma sugestão?

John Tobler
fonte
1
@carlmanaster, você está perto o suficiente para mim! Estou colocando isso no meu próximo TestFixture para testá-lo. É como aquele pequeno lembrete para fazer o que sua mãe deveria ter ensinado: "Se abrir feche! Se bagunçar, limpe!" Talvez outra pessoa possa melhorar, mas pelo menos começa com um "a!" Obrigado por sua sugestão!
John Tobler
1
@carlmanaster, eu tentei "Anular". É melhor do que "demolir" e meio que funciona, mas ainda estou procurando outra palavra "A" que fique na minha cabeça tão perfeitamente quanto "Organizar, agir, afirmar". Talvez "Aniquilar ?!"
John Tobler de
1
Então, agora, eu tenho "Organizar, Assumir, Agir, Assertar, Aniquilar". Hmmm! Estou complicando as coisas, hein? Talvez seja melhor eu apenas beijar e voltar para "Arrange, Act, and Assert!"
John Tobler de
1
Talvez use um R para reiniciar? Eu sei que não é um A, mas parece um pirata dizendo: Aaargh! e Reset rima com Assert: o
Marcel Valdez Orozco
1

Dê uma olhada na entrada da Wikipedia sobre Design by Contract . A sagrada trindade Arrange-Act-Assert é uma tentativa de codificar alguns dos mesmos conceitos e é sobre como provar a correção do programa. Do artigo:

The notion of a contract extends down to the method/procedure level; the
contract for each method will normally contain the following pieces of
information:

    Acceptable and unacceptable input values or types, and their meanings
    Return values or types, and their meanings
    Error and exception condition values or types that can occur, and their meanings
    Side effects
    Preconditions
    Postconditions
    Invariants
    (more rarely) Performance guarantees, e.g. for time or space used

Existe uma compensação entre a quantidade de esforço despendido na configuração e o valor que isso agrega. AAA é um lembrete útil para as etapas mínimas necessárias, mas não deve desencorajar ninguém de criar etapas adicionais.

David Clarke
fonte
0

Depende do seu ambiente / idioma de teste, mas geralmente se algo na parte Arrange falhar, uma exceção é lançada e o teste falha exibindo-o em vez de iniciar a parte Act. Então, não, eu geralmente não uso uma segunda parte Assert.

Além disso, no caso de sua parte Arrange ser bastante complexa e nem sempre lançar uma exceção, talvez você possa considerar envolvê-la em algum método e escrever um teste próprio para ela, para ter certeza de que não falhará (sem lançando uma exceção).

schnaader
fonte
0

Não uso esse padrão, porque acho que fazer algo como:

Arrange
Assert-Not
Act
Assert

Pode ser inútil, porque supostamente você sabe que sua parte Arrange funciona corretamente, o que significa que tudo o que está na parte Arrange deve ser testado também ou ser simples o suficiente para não precisar de testes.

Usando o exemplo da sua resposta:

public void testEncompass() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7)); // <-- Pointless and against DRY if there 
                                    // are unit tests for Range(int, int)
    range.encompass(7);
    assertTrue(range.includes(7));
}
Marcel Valdez Orozco
fonte
Receio que você realmente não tenha entendido minha pergunta. A afirmação inicial não é sobre testar Arrange; é simplesmente garantir que a Lei é o que faz com que o estado seja afirmado no final.
Carl Manaster de
E meu ponto é que, tudo o que você colocar na parte Assert-Not, já está implícito na parte Arrange, porque o código na parte Arrange foi completamente testado e você já sabe o que ele faz.
Marcel Valdez Orozco
Mas acredito que haja valor na parte Assert-Not, porque você está dizendo: Dado que a parte Arrange deixa 'o mundo' neste 'estado', então meu 'Act' deixará 'o mundo' neste 'novo estado' ; e se a implementação do código do qual a parte Arrange depender mudar, o teste também será interrompido. Mas, novamente, isso pode ser contra o DRY, porque você (deveria) também ter testes para qualquer código que esteja dependendo na parte Arrange.
Marcel Valdez Orozco
Talvez em projetos onde existem várias equipes (ou uma grande equipe) trabalhando no mesmo projeto, tal cláusula seria muito útil, caso contrário, considero-a desnecessária e redundante.
Marcel Valdez Orozco
Provavelmente, tal cláusula seria melhor em testes de integração, testes de sistema ou testes de aceitação, onde a parte Arrange geralmente depende de mais de um componente, e há mais fatores que podem fazer com que o estado inicial do 'mundo' mude inesperadamente. Mas não vejo um lugar para isso nos testes de unidade.
Marcel Valdez Orozco
0

Se você realmente deseja testar tudo no exemplo, tente mais testes ... como:

public void testIncludes7() throws Exception {
    Range range = new Range(0, 5);
    assertFalse(range.includes(7));
}

public void testIncludes5() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(5));
}

public void testIncludes0() throws Exception {
    Range range = new Range(0, 5);
    assertTrue(range.includes(0));
}

public void testEncompassInc7() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(7));
}

public void testEncompassInc5() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(5));
}

public void testEncompassInc0() throws Exception {
    Range range = new Range(0, 5);
    range.encompass(7);
    assertTrue(range.includes(0));
}

Porque, caso contrário, você perderá tantas possibilidades de erro ... por exemplo, após englobar, o intervalo inclui apenas 7, etc ... Existem também testes para o comprimento do intervalo (para garantir que também não abrange um valor aleatório), e outro conjunto de testes inteiramente para tentar englobar 5 no intervalo ... o que esperaríamos - uma exceção em englobar ou o intervalo inalterado?

De qualquer forma, a questão é se houver alguma suposição no ato que você deseja testar, coloque-a em seu próprio teste, certo?

Andrew
fonte
0

Eu uso:

1. Setup
2. Act
3. Assert 
4. Teardown

Porque uma configuração limpa é muito importante.

kame
fonte