ti-enxame.com

Tudo bem ter várias afirmações em um único teste de unidade?

No comentário a este ótimo post , Roy Osherove mencionou o projeto OAPT que foi projetado para executar cada afirmação em um único teste.

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

Os testes de unidade adequados devem falhar por exatamente um motivo , é por isso que você deve usar uma declaração por teste de unidade.

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

Minha orientação é geralmente que você teste um CONCEITO lógico por teste. você pode ter várias declarações no mesmo objeto. eles geralmente serão o mesmo conceito sendo testado.

Penso que, em alguns casos, são necessárias várias asserções (por exemplo, Guard Assertion ), mas, em geral, tento evitar isso. qual e sua OPINIAO? Por favor, forneça um exemplo do mundo real, onde várias afirmações são realmente necessárias.

430
Restuta

Eu não acho que seja necessariamente uma coisa ruim , mas acho que devemos nos esforçar para ter apenas declaraçõ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 tenha apenas uma afirmação. Eu acho que ele só se torna um cheiro de código (teste?) quando você tem cerca de cinco ou mais afirmações em seu teste.

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

246
Jaco Pretorius

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

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

Fico feliz em ver várias declarações de afirmação que fazem parte 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 afirmação, mas isso é diferente de insistir que você deveria ou deve. Não há melhoria 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 a combinação de duas ou três (ou mais) afirmações em uma grande condição booleana torna mais difícil de ler, mais difícil de alterar e mais difícil de descobrir por que falhou. Por que fazer isso apenas por uma regra?

[~ # ~] nb [~ # ~] : O código que estou escrevendo aqui é C # com NUnit, mas os princípios se mantêm em outros idiomas e estruturas. A sintaxe também pode ser muito semelhante.

314
Anthony

Eu nunca pensei que mais de uma afirmação fosse 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 consigo pensar no teste.

85
Matt Ellen

Quando estou usando o teste de unidade para validar o comportamento de alto nível, absolutamente coloco várias asserções em um único teste. Aqui está um teste que estou usando para algum código de notificação de emergência. O código 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 todas as etapas do processo para que eu tenha certeza de que o código está se comportando da maneira que eu espero. Se uma única afirmação falhar, não me importo que as demais nem sejam executadas; porque o estado do sistema não é mais válido, essas afirmações subseqüentes não me revelariam nada valioso. * Se assertAllUnitsAlerting() falhou, não saberia o que fazer com assertAllNotificationSent() 's success OR falha até determinar o que estava causando o erro anterior e corrigi-lo.

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

21
BlairHippo

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

class Service {
    Result process();
}

class Result {
    Inner inner;
}

class Inner {
    int number;
}

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

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() );
}
8
Betlista

Eu acho que existem 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 sequência de datas:

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, é devido a um motivo: a análise está incorreta. Se você argumentar que esse teste pode falhar por três razões diferentes, IMHO seria muito refinado na definição de "uma razão".

6
Pete

Não conheço nenhuma situação em que seria uma boa ideia ter várias asserções dentro do próprio método [Teste]. A principal razão pela qual as pessoas gostam de ter várias asserções é que 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 pelas quais o código pode não ter reagido da maneira esperada, em vez de apenas aquele em que a primeira afirmaçã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 um deles afirmando coisas diferentes que você esperaria serem verdadeiras, dado o estado atual do objeto. Cada método [Teste] recebe o nome da coisa que está afirmando, exceto, talvez, o nome do conceito, em vez de apenas uma leitura em inglês do código. Então, cada implementação do método [Test] precisa apenas de uma única linha de código em que está afirmando alguma coisa. Outra vantagem dessa abordagem é tornar os testes muito legíveis, pois fica bem claro o que você está testando e o que você espera apenas olhando para os nomes de classe e método. Isso também será melhor quando você perceber todos os pequenos casos do Edge que deseja testar e encontrar bugs.

Geralmente, isso significa que a linha final de código dentro do método [SetUp] deve armazenar um valor de propriedade ou retornar um valor em uma variável de instância privada do [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 em teste estão definidas agora que estão no estado desejado.

Às vezes, você precisa fazer afirmações ao longo do caminho, ao colocar o objeto em teste no estado desejado, para garantir que você não estrague tudo 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 chegasse ao estado desejado que você pretendia testar.

Outro problema que você pode encontrar é que você pode estar testando uma exceção que você esperava que fosse lançada. Isso pode levá-lo a não seguir o modelo acima. No entanto, isso ainda pode ser alcançado capturando a exceção dentro do método [SetUp] e armazenando-a em uma variável de instância. Isso permitirá que você afirme coisas diferentes sobre a exceção, cada uma no seu próprio método [Teste]. Você também pode afirmar outras coisas sobre o objeto em teste para garantir que não haja efeitos colaterais indesejados da exceção 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));
        }
    }
}
3
still_dreaming_1

Se você tiver várias afirmações em uma única função de teste, espero que sejam diretamente relevantes para o teste que você está realizando. 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ê sente que provavelmente é um exagero) não é uma coisa ruim. Você pode argumentar que ter os testes vitais e mais essenciais é mais importante. Portanto, quando você estiver afirmando, verifique se suas declarações de afirmação estão corretamente colocadas, em vez de se preocupar demais com várias afirmações. Se você precisar de mais de um, use mais de um.

2
hagubear

Ter várias asserções no mesmo teste é apenas um problema quando o teste falha. Talvez você precise depurar o teste ou analisar a exceção para descobrir qual afirmação é que 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 asserções sejam realmente necessárias, pois você sempre pode reescrevê-las como várias condições na mesma asserção. No entanto, pode ser preferível se você, por exemplo, tiver várias etapas para verificar os dados intermediários entre as etapas, em vez de arriscar que as etapas posteriores falhem devido a uma entrada incorreta.

2
Guffa

Se o seu teste falhar, você não saberá se as afirmações a seguir também serão quebradas. Muitas vezes, isso significa que você estará perdendo informações valiosas para descobrir a origem do problema. Minha solução é usar uma afirmaçã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 ao mesmo tempo. Uso várias linhas porque a maioria dos IDEs exibirá diferenças de seqüência de caracteres em um diálogo de comparação lado a lado.

2
Aaron Digulla

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á, uma vez que outra afirmação falhar ou, em outras palavras, existe um relacionamento de dependência entre o teste, faz sentido lançá-las como múltiplas afirmações em um único teste. Isso tem o benefício de não desarrumar os resultados do teste com falhas óbvias que poderiam ter sido eliminadas se resgatássemos a primeira afirmação em um único teste. No caso em que esse relacionamento não exista, a preferência seria, naturalmente, separar essas asserções em testes individuais porque, caso contrário, encontrar essas falhas exigiria várias iterações de execuções de teste para solucionar todos os problemas.

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

1
jpierson

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

Os testes de unidade adequados devem falhar por exatamente um motivo, por isso você deve usar uma afirmação por teste de unidade.

Nunca achei essas formulações úteis (o fato de uma classe ter um motivo para mudar é um exemplo de um ditado inútil). Considere uma afirmação de que duas seqüências de caracteres são iguais, isso é semanticamente equivalente a afirmar que o comprimento das duas seqüências de caracteres é o mesmo e que cada caractere no índice correspondente é igual.

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

Portanto, concentre-se apenas na clareza do código e na clareza dos resultados do teste e deixe-o guiar o número de afirmações usadas em vez de vice-versa.

1
CurtainDog

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

Ter múltiplas afirmações pode facilmente entrar no problema do 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 vários testes em uma lasanha grande, tornando difícil descobrir qual teste faz o impossível. .

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

0
gsf

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

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

Adote o conceito lógico de uma transação bancária - retirar um valor de uma conta bancária na maioria dos casos DEVE incluir adicionar esse valor a outra conta. Você NUNCA deseja 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 deseja garantir que a transação funcione. Nesse caso, simplesmente não é suficiente para garantir que as etapas individuais tenham êxito. Você deve verificar as duas contas bancárias no 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 de 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 a testes de unidade.

0
cslotty