É aceitável ter várias afirmações em um único teste unitário?

343

No comentário a este excelente post , Roy Osherove mencionou o projeto OAPT projetado para executar cada declaração em um único teste.

O seguinte está escrito na página inicial do projeto:

Proper unit tests should fail for exactly one reason, that’s why you should be using one assert per unit test.

E, também, Roy escreveu em comentários:

My guideline is usually that you test one logical CONCEPT per test. you can have multiple asserts on the same object. they will usually be the same concept being tested.

Acho que há alguns casos em que várias afirmações são necessárias (por exemplo, Asserção de Guarda ), mas em geral eu tente evitar isso. qual e sua OPINIAO? Forneça um exemplo de palavra real em que várias afirmações são realmente necessárias .

    
por Restuta 02.06.2016 / 11:12
fonte

15 respostas

210

Eu não acho que seja necessariamente uma coisa ruim , mas eu acho que devemos nos esforçarmos para ter apenas afirmações únicas em nossos testes. Isso significa que você escreve muito mais testes e nossos testes acabariam testando apenas uma coisa de cada vez.

Dito isto, eu diria que talvez metade dos meus testes tenham apenas uma afirmação. Eu acho que só se torna um código (teste?) Cheiro quando você tem cerca de cinco ou mais afirmações no seu teste.

Como você resolve várias afirmações?

    
por 12.05.2015 / 15:03
fonte
242

Os testes devem falhar apenas por um motivo, mas isso nem sempre significa que deve haver apenas uma instrução Assert . IMHO é mais importante manter o padrão " Organizar, Agir, Declarar ".

A chave é que você tem apenas uma ação e, em seguida, inspeciona os resultados dessa ação usando as afirmações. Mas é "Organizar, Agir, Assert, Fim do teste ". Se você estiver tentado a continuar testando, realizando outra ação e mais declarações depois, faça um teste separado.

Fico feliz em ver várias instruções de declaração que formam partes do teste da mesma ação. por exemplo,

[Test]
public void ValueIsInRange()
{
  int value = GetValueToTest();

  Assert.That(value, Is.GreaterThan(10), "value is too small");
  Assert.That(value, Is.LessThan(100), "value is too large");
} 

ou

[Test]
public void ListContainsOneValue()
{
  var list = GetListOf(1);

  Assert.That(list, Is.Not.Null, "List is null");
  Assert.That(list.Count, Is.EqualTo(1), "Should have one item in list");
  Assert.That(list[0], Is.Not.Null, "Item is null");
} 

Você poderia combiná-los em uma declaração, mas isso é diferente de insistir que você deveria ou deve . Não há melhorias em combiná-los.

por exemplo. O primeiro poderia ser

Assert.IsTrue((10 < value) && (value < 100), "Value out of range"); 

Mas isso não é melhor - a mensagem de erro é menos específica e não tem outras vantagens. Tenho certeza que você pode pensar em outros exemplos em que combinar duas ou três (ou mais) afirmações em uma grande condição booleana torna mais difícil a leitura, mais difícil de alterar e mais difícil descobrir por que ela falhou. Por que isso apenas por uma regra?

NB : O código que estou escrevendo aqui é o C # com o NUnit, mas os princípios serão mantidos em outras linguagens e frameworks. A sintaxe pode ser muito semelhante também.

    
por 12.05.2015 / 15:04
fonte
83

Eu nunca pensei que mais de uma afirmação fosse uma coisa ruim.

Eu faço isso o tempo todo:

public void ToPredicateTest()
{
    ResultField rf = new ResultField(ResultFieldType.Measurement, "name", 100);
    Predicate<ResultField> p = (new ConditionBuilder()).LessThanConst(400)
                                                       .Or()
                                                       .OpenParenthesis()
                                                       .GreaterThanConst(500)
                                                       .And()
                                                       .LessThanConst(1000)
                                                       .And().Not()
                                                       .EqualsConst(666)
                                                       .CloseParenthesis()
                                                       .ToPredicate();
    Assert.IsTrue(p(ResultField.FillResult(rf, 399)));
    Assert.IsTrue(p(ResultField.FillResult(rf, 567)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 400)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 666)));
    Assert.IsFalse(p(ResultField.FillResult(rf, 1001)));

    Predicate<ResultField> p2 = (new ConditionBuilder()).EqualsConst(true).ToPredicate();

    Assert.IsTrue(p2(new ResultField(ResultFieldType.Confirmation, "Is True", true)));
    Assert.IsFalse(p2(new ResultField(ResultFieldType.Confirmation, "Is False", false)));
}

Aqui, uso várias afirmações para garantir que condições complexas possam ser transformadas no predicado esperado.

Estou testando apenas uma unidade (o método ToPredicate ), mas estou cobrindo tudo o que posso imaginar no teste.

    
por 23.03.2014 / 08:20
fonte
18

Quando estou usando o teste de unidade para validar o comportamento de alto nível, eu absolutamente coloco várias asserções em um único teste. Aqui está um teste que estou realmente usando para algum código de notificação de emergência. O código que é executado antes do teste coloca o sistema em um estado em que, se o processador principal for executado, um alarme será enviado.

@Test
public void testAlarmSent() {
    assertAllUnitsAvailable();
    assertNewAlarmMessages(0);

    pulseMainProcessor();

    assertAllUnitsAlerting();
    assertAllNotificationsSent();
    assertAllNotificationsUnclosed();
    assertNewAlarmMessages(1);
}

Ele representa as condições que precisam existir em cada etapa do processo para que eu tenha certeza de que o código está se comportando da maneira esperada. Se uma única afirmação falhar, não me importo que os restantes nem sequer sejam executados; porque o estado do sistema não é mais válido, as afirmações subsequentes não me diziam nada valioso. * Se assertAllUnitsAlerting() falhasse, então eu não saberia o que fazer com o sucesso de assertAllNotificationSent() OU falha até determinar o que estava causando o erro anterior e o corrigiu.

(* - Ok, eles podem ser úteis na depuração do problema. Mas as informações mais importantes, que o teste falhou, já foram recebidas.)

    
por 12.05.2015 / 15:01
fonte
8

Outra razão pela qual acredito que várias afirmações em um método não são uma coisa ruim são descritas no código a seguir:

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

No meu teste, simplesmente quero testar se service.process() retorna o número correto em Inner instâncias de classe.

Em vez de testar ...

@Test
public void test() {
    Result res = service.process();
    if ( res != null && res.getInner() != null ) Assert.assertEquals( ..., res.getInner() );
}

Estou fazendo

@Test
public void test() {
    Result res = service.process();
    Assert.notNull(res);
    Assert.notNull(res.getInner());
    Assert.assertEquals( ..., res.getInner() );
}
    
por 30.10.2012 / 15:21
fonte
6

Acho que há muitos casos em que escrever várias afirmações é válido dentro da regra de que um teste só deve falhar por um motivo.

Por exemplo, imagine uma função que analise uma string de data:

function testParseValidDateYMD() {
    var date = Date.parse("2016-01-02");

    Assert.That(date.Year).Equals(2016);
    Assert.That(date.Month).Equals(1);
    Assert.That(date.Day).Equals(0);
}

Se o teste falhar, é por causa de um motivo, a análise está incorreta. Se você argumentar que este teste pode falhar por três razões diferentes, você poderia IMHO ser muito refinado na sua definição de "uma razão".

    
por 02.06.2016 / 13:00
fonte
2

Ter várias asserções no mesmo teste é apenas um problema quando o teste falha. Em seguida, você pode ter que depurar o teste ou analisar a exceção para descobrir qual afirmação falha. Com uma afirmação em cada teste, geralmente é mais fácil identificar o que está errado.

Não consigo pensar em um cenário em que várias afirmações sejam realmente necessárias , pois você sempre pode reescrevê-las como várias condições na mesma asserção. Pode, no entanto, ser preferível se, por exemplo, você tiver várias etapas para verificar os dados intermediários entre as etapas, em vez de arriscar que as etapas posteriores travem devido a uma entrada incorreta.

    
por 28.09.2010 / 16:05
fonte
2

Se o seu teste falhar, você não saberá se as seguintes afirmações também serão violadas. Muitas vezes, isso significa que você perderá informações valiosas para descobrir a origem do problema. Minha solução é usar uma declaração, mas com vários valores:

String actual = "val1="+val1+"\nval2="+val2;
assertEquals(
    "val1=5\n" +
    "val2=hello"
    , actual
);

Isso me permite ver todas as afirmações com falha de uma só vez. Eu uso várias linhas porque a maioria dos IDEs exibirá diferenças de string em um diálogo de comparação lado a lado.

    
por 28.09.2010 / 17:22
fonte
2

Se você tiver várias afirmações em uma única função de teste, espero que elas sejam diretamente relevantes para o teste que você está conduzindo. Por exemplo,

@Test
test_Is_Date_segments_correct {

   // It is okay if you have multiple asserts checking dd, mm, yyyy, hh, mm, ss, etc. 
   // But you would not have any assert statement checking if it is string or number,
   // that is a different test and may be with multiple or single assert statement.
}

Ter muitos testes (mesmo quando você acha que é um exagero) não é uma coisa ruim. Você pode argumentar que ter os testes vitais e mais essenciais é mais importante. Então, quando você está afirmando, certifique-se de que suas assert afirmações estão colocadas corretamente ao invés de se preocupar com múltiplas afirmações demais. Se você precisar de mais de um, use mais de um.

    
por 12.05.2015 / 15:01
fonte
2

Eu não sei de nenhuma situação em que seria uma boa ideia ter várias afirmações dentro do próprio método [Test]. A principal razão pela qual as pessoas gostam de ter várias Asserções é que elas estão tentando ter uma classe [TestFixture] para cada classe que está sendo testada. Em vez disso, você pode dividir seus testes em mais classes [TestFixture]. Isso permite que você veja várias maneiras em que o código pode não ter reagido da maneira esperada, em vez de apenas aquele em que a primeira declaração falhou. A maneira como você consegue isso é que você tem pelo menos um diretório por classe sendo testado com muitas classes [TestFixture] dentro. Cada classe [TestFixture] seria nomeada após o estado específico de um objeto que você estará testando. O método [SetUp] colocará o objeto no estado descrito pelo nome da classe. Então você tem vários métodos [Test] cada afirmando coisas diferentes que você esperaria que sejam verdadeiras, dado o estado atual do objeto. Cada método [Test] é nomeado após a coisa que está afirmando, exceto, talvez, que possa ser nomeado após o conceito, em vez de apenas uma leitura em inglês do código. Em seguida, cada implementação do método [Test] só precisa de uma única linha de código em que esteja afirmando algo. Outra vantagem dessa abordagem é tornar os testes muito legíveis, pois fica bastante claro o que você está testando e o que você espera apenas observando os nomes de classes e métodos. Isso também será melhorado à medida que você começar a perceber todos os pequenos casos de borda que deseja testar e ao encontrar bugs.

Geralmente, isso significa que a linha final de código dentro do método [SetUp] deve armazenar um valor de propriedade ou um valor de retorno em uma variável de instância privada de [TestFixture]. Então você pode afirmar várias coisas diferentes sobre essa variável de instância a partir de diferentes métodos [Test]. Você também pode fazer afirmações sobre quais propriedades diferentes do objeto sob teste estão definidas agora que está no estado desejado.

Às vezes, você precisa fazer afirmações ao longo do caminho, à medida que estiver obtendo o objeto em teste no estado desejado, a fim de garantir que não estragou antes de colocar o objeto no estado desejado. Nesse caso, essas asserções extras devem aparecer dentro do método [SetUp]. Se algo der errado dentro do método [SetUp], ficará claro que algo estava errado com o teste antes que o objeto tenha chegado ao estado desejado que você pretendia testar.

Um outro problema que você pode encontrar é que você pode estar testando uma exceção que você esperava que fosse lançada. Isso pode fazer com que você não siga o modelo acima. No entanto, ele ainda pode ser alcançado pegando a exceção dentro do método [SetUp] e armazenando-a em uma variável de instância. Isso permitirá que você declare coisas diferentes sobre a exceção, cada uma em seu próprio método [Test]. Você também pode afirmar outras coisas sobre o objeto sob teste para garantir que não houve efeitos colaterais indesejados da exceção que está sendo lançada.

Exemplo (isso seria dividido em vários arquivos):

namespace Tests.AcctTests
{
    [TestFixture]
    public class no_events
    {
        private Acct _acct;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
        }

        [Test]
        public void balance_0() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_0
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(0m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }

    [TestFixture]
    public class try_withdraw_negative
    {
        private Acct _acct;
        private List<string> _problems;

        [SetUp]
        public void SetUp() {
            _acct = new Acct();
            Assert.That(_acct.Balance, Is.EqualTo(0));
            _problems = _acct.Withdraw(-0.01m);
        }

        [Test]
        public void has_problem() {
            Assert.That(_problems, Is.EquivalentTo(new string[] { "Withdraw amount must be greater than zero." }));
        }

        [Test]
        public void balance_not_changed() {
            Assert.That(_acct.Balance, Is.EqualTo(0m));
        }
    }
}
    
por 12.05.2015 / 15:06
fonte
1

O objetivo do teste de unidade é fornecer o máximo de informações possível sobre o que está falhando, mas também ajudar a identificar com precisão os problemas mais fundamentais primeiro. Quando você sabe logicamente que uma afirmação falhará, dado que outra afirmação falha ou, em outras palavras, existe uma relação de dependência entre o teste, então faz sentido revê-las como várias afirmações em um único teste. Isso tem o benefício de não colocar no lixo os resultados do teste com falhas óbvias que poderiam ter sido eliminadas se nós desistíssemos da primeira afirmação em um único teste. No caso em que esse relacionamento não existe, a preferência seria, então, separar essas asserções em testes individuais, pois, caso contrário, encontrar essas falhas exigiria várias iterações de execuções de teste para resolver todos os problemas.

Se você também projetar as unidades / classes de forma que testes excessivamente complexos precisem ser escritos, isso causará menos sobrecarga durante o teste e provavelmente promoverá um design melhor.

    
por 15.03.2013 / 23:00
fonte
0

Sim, não há problema em ter várias afirmações desde que um teste com falha forneça informações suficientes para que você possa diagnosticar a falha. Isso vai depender do que você está testando e quais são os modos de falha.

Proper unit tests should fail for exactly one reason, that’s why you should be using one assert per unit test.

Eu nunca achei que tais formulações fossem úteis (uma classe deveria ter um motivo para mudar é um exemplo de tal ditado inútil). Considere uma afirmação de que duas strings são iguais, isso é semanticamente equivalente a afirmar que o comprimento das duas strings é o mesmo e que cada caracter no índice correspondente é igual.

Poderíamos generalizar e dizer que qualquer sistema de múltiplas asserções poderia ser reescrito como uma afirmação única, e qualquer afirmação única poderia ser decomposta em um conjunto de assertivas menores.

Portanto, concentre-se apenas na clareza do código e na clareza dos resultados do teste, e deixe que isso guie o número de asserções usadas, e não vice-versa.

    
por 19.04.2018 / 06:48
fonte
0

A resposta é muito simples - se você testar uma função que altera mais de um atributo, do mesmo objeto ou até mesmo de dois objetos diferentes, e a correção da função depende dos resultados de todas essas mudanças, então você quero afirmar que cada uma dessas mudanças foi realizada corretamente!

Eu tenho a idéia de um conceito lógico, mas a conclusão inversa diria que nenhuma função deve mudar mais do que um objeto. Mas isso é impossível de implementar em todos os casos, na minha experiência.

Considere o conceito lógico de uma transação bancária - na maioria dos casos, a retirada de uma quantia de uma conta bancária inclui a adição dessa quantia a outra conta. Você NUNCA quer separar essas duas coisas, elas formam uma unidade atômica. Você pode querer fazer duas funções (retirar / adicionar dinheiro) e, assim, escrever dois testes de unidade diferentes - além disso. Mas essas duas ações devem ocorrer dentro de uma transação e você também quer ter certeza de que a transação funciona. Nesse caso, simplesmente não é suficiente para garantir que as etapas individuais sejam bem-sucedidas. Você precisa verificar as duas contas bancárias em seu teste.

Pode haver exemplos mais complexos que você não testaria em um teste de unidade, em primeiro lugar, mas em um teste de integração ou aceitação. Mas esses limites são fluentes, IMHO! Não é tão fácil decidir, é uma questão de circunstâncias e talvez preferência pessoal. Retirar dinheiro de um e adicioná-lo a outra conta ainda é uma função muito simples e definitivamente um candidato para testes de unidade.

    
por 12.12.2018 / 16:45
fonte
-1

Esta questão está relacionada ao problema clássico de balanceamento entre problemas de código de espaguete e lasanha.

Ter várias afirmações pode facilmente entrar no problema de espaguete, onde você não tem idéia do que é o teste, mas ter uma única afirmação por teste pode tornar seu teste igualmente ilegível, com múltiplos testes em uma grande lasanha. faz o que é impossível.

Existem algumas exceções, mas, neste caso, manter o pêndulo no meio é a resposta.

    
por 11.06.2016 / 00:22
fonte
-3

Eu nem concordo com a "falha por apenas uma razão" em geral. O que é mais importante é que os testes são curtos e leem claramente imo.

Isso nem sempre é possível, e quando um teste é complicado, um nome descritivo (longo) e menos testes fazem mais sentido.

    
por 13.04.2014 / 00:10
fonte