Python unittest - o oposto de assertRaises?


374

Quero escrever um teste para estabelecer que uma exceção não é gerada em uma determinada circunstância.

É simples testar se uma exceção é gerada ...

sInvalidPath=AlwaysSuppliesAnInvalidPath()
self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath) 

... mas como você pode fazer o oposto .

Algo assim, o que eu estou procurando ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 

6
Você sempre pode simplesmente fazer o que deve funcionar no teste. Se ocorrer um erro, isso será exibido (contado como um erro, e não como uma falha). Obviamente, isso pressupõe que não gera nenhum erro, e não apenas um tipo definido de erro. Fora isso, acho que você teria que escrever o seu próprio.
Thomas K


Acontece que você pode de fato implementar um assertNotRaisesmétodo que compartilha 90% de seu código / comportamento assertRaisesem cerca de ~ 30 linhas de código. Veja minha resposta abaixo para obter detalhes.
tel

Eu quero isso para que eu possa comparar duas funções hypothesispara garantir que elas produzam a mesma saída para todos os tipos de entrada, ignorando os casos em que o original gera uma exceção. assume(func(a))não funciona porque a saída pode ser uma matriz com um valor de verdade ambíguo. Então, eu só quero chamar uma função e obter Truese ela não falhar. assume(func(a) is not None)funciona eu acho
endolith

Respostas:


394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")

32
@hiwaylon - Não, esta é a solução correta de fato. A solução proposta por user9876 é conceitualmente defeituosa: se você testar o não aumento de dizer ValueError, mas ValueErrorfor aumentado, seu teste deverá sair com uma condição de falha, não com erro. Por outro lado, se executando o mesmo código, você geraria um KeyError, isso seria um erro, não uma falha. Em python - diferentemente de outras linguagens - as exceções são rotineiramente usadas para o fluxo de controle, é por isso que temos a except <ExceptionName>sintaxe de fato. A esse respeito, a solução do user9876 está simplesmente errada.
mac

@mac - Essa também é uma solução correta? stackoverflow.com/a/4711722/6648326
MasterJoe2

Este tem o efeito infeliz de mostrar <100% de cobertura (a exceção nunca acontecerá) para testes.
Shay

3
@Shay, IMO você deve sempre excluir os arquivos de teste se dos relatórios de cobertura (como eles funcionam quase sempre 100% por definição, você seria inflar artificialmente os relatórios)
Molho de churrasco Original

@ molho-churrasco original, isso não me deixaria aberto a testes ignorados sem querer. Por exemplo, erro de digitação no nome do teste (função ttst_), configuração de execução incorreta no pycharm, etc.?
Shay

67

Oi - Quero escrever um teste para estabelecer que uma exceção não é gerada em uma determinada circunstância.

Essa é a suposição padrão - exceções não são levantadas.

Se você não disser mais nada, isso é assumido em todos os testes.

Você não precisa escrever uma afirmação para isso.


7
@IndradhanushGupta Bem, a resposta aceita torna o teste mais pitônico do que este. Explícito é melhor que implícito.
0xc0de 29/10/2015

17
Nenhum outro comentarista apontou por que esta resposta está errada, embora seja a mesma razão que a resposta do usuário9876 esteja errada: falhas e erros são coisas diferentes no código de teste. Se sua função fosse para lançar uma exceção durante um teste que não afirma, a estrutura de teste iria tratar isso como um erro , em vez de uma falha em não assert.
Coredumperror

@CoreDumpError Entendo a diferença entre uma falha e um erro, mas isso não força você a cercar todos os testes com uma construção de tentativa / exceção? Ou você recomendaria fazer isso apenas para testes que geram explicitamente uma exceção sob alguma condição (o que basicamente significa que a exceção é esperada ).
Federicojasson

4
@federicojasson Você respondeu sua própria pergunta muito bem nessa segunda frase. Erros x falhas nos testes podem ser descritos sucintamente como "falhas inesperadas" vs "comportamento não intencional", respectivamente. Você deseja que seus testes mostrem um erro quando sua função falha, mas não quando uma exceção que você sabe que será lançada, pois determinadas entradas são lançadas quando são fornecidas entradas diferentes.
Coredumperror

52

Basta chamar a função. Se houver uma exceção, a estrutura de teste da unidade sinalizará isso como um erro. Você pode adicionar um comentário, por exemplo:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)

35
Falhas e erros são conceitualmente diferentes. Além disso, como em python as exceções são rotineiramente usadas para o fluxo de controle, isso tornará muito difícil de entender rapidamente (= sem explorar o código de teste) se você quebrou sua lógica ou seu código ...
mac

11
O seu teste passa ou não. Se não passar, você precisará corrigi-lo. Se é relatado como uma "falha" ou um "erro", é principalmente irrelevante. Há uma diferença: com a minha resposta, você verá o rastreamento da pilha para poder ver onde PathIsNotAValidOne foi lançado; com a resposta aceita, você não terá essas informações, portanto a depuração será mais difícil. (Supondo Py2; não tenho certeza se Py3 é melhor nisso).
user9876

19
@ user9876 - Não. As condições de saída do teste são 3 (aprovação / aprovação / erro), e não 2, como você acredita erroneamente. A diferença entre erros e falhas é substancial e tratá-los como se fossem iguais é apenas uma programação ruim. Se você não acredita em mim, veja como os executores de teste funcionam e quais árvores de decisão eles implementam para falhas e erros. Um bom ponto de partida para python é o xfaildecorador em pytest.
mac

4
Eu acho que depende de como você usa testes de unidade. Do jeito que minha equipe usa testes de unidade, eles têm que passar. (Programação ágil, com uma máquina de integração contínua que executa todos os testes de unidade). Eu sei que o caso de teste pode relatar "aprovação", "falha" ou "erro". Mas em alto nível, o que realmente importa para minha equipe é "todos os testes de unidade são aprovados?" (ou seja, "Jenkins é verde?"). Portanto, para minha equipe, não há diferença prática entre "falha" e "erro". Você pode ter requisitos diferentes se usar os testes de unidade de uma maneira diferente.
user9876

11
@ user9876 a diferença entre 'falha' e 'erro' é a diferença entre "a minha declaração falhou" e "o meu teste nem chega à declaração". Essa, para mim, é uma distinção útil durante a correção de testes, mas acho que, como você diz, não é para todos.
CS

14

Sou o pôster original e aceitei a resposta acima pela DGH sem antes usá-la no código.

Uma vez que eu usei, percebi que precisava de alguns ajustes para realmente fazer o que eu precisava (para ser justo com a DGH, ele / ela disse "ou algo semelhante"!).

Eu pensei que valia a pena postar o tweak aqui para o benefício de outros:

    try:
        a = Application("abcdef", "")
    except pySourceAidExceptions.PathIsNotAValidOne:
        pass
    except:
        self.assertTrue(False)

O que eu estava tentando fazer aqui era garantir que, se fosse feita uma tentativa de instanciar um objeto Application com um segundo argumento de espaços, o pySourceAidExceptions.PathIsNotAValidOne seria gerado.

Acredito que o uso do código acima (baseado fortemente na resposta da DGH) fará isso.


2
Como você está esclarecendo sua pergunta e não a responde, você deve editá-la (não responder). Consulte a minha resposta abaixo.
hiwaylon

13
Parece ser exatamente o oposto do problema original. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)deve fazer o trabalho neste caso.
Antony Hatchkins

8

Você pode definir assertNotRaisesreutilizando cerca de 90% da implementação original assertRaisesno unittestmódulo. Com essa abordagem, você acaba com um assertNotRaisesmétodo que, além da condição de falha reversa, se comporta de maneira idêntica assertRaises.

TLDR e demonstração ao vivo

É surpreendentemente fácil adicionar um assertNotRaisesmétodo unittest.TestCase(levei cerca de 4 vezes mais tempo para escrever essa resposta do que o código). Aqui está uma demonstração ao vivo do assertNotRaisesmétodo em ação . Assim comoassertRaises , você pode passar um call e args para assertNotRaises, ou você pode usá-lo em uma withdeclaração. A demonstração ao vivo inclui casos de teste que demonstram que assertNotRaisesfuncionam conforme o esperado.

Detalhes

A implementação de assertRaisesin unittesté bastante complicada, mas com uma subclasse inteligente, você pode substituir e reverter sua condição de falha.

assertRaisesé um método curto que basicamente apenas cria uma instância da unittest.case._AssertRaisesContextclasse e a retorna (veja sua definição no unittest.casemódulo). Você pode definir sua própria _AssertNotRaisesContextclasse subclassificando _AssertRaisesContexte substituindo seu __exit__método:

import traceback
from unittest.case import _AssertRaisesContext

class _AssertNotRaisesContext(_AssertRaisesContext):
    def __exit__(self, exc_type, exc_value, tb):
        if exc_type is not None:
            self.exception = exc_value.with_traceback(None)

            try:
                exc_name = self.expected.__name__
            except AttributeError:
                exc_name = str(self.expected)

            if self.obj_name:
                self._raiseFailure("{} raised by {}".format(exc_name,
                    self.obj_name))
            else:
                self._raiseFailure("{} raised".format(exc_name))

        else:
            traceback.clear_frames(tb)

        return True

Normalmente você define classes de caso de teste, herdando-as de TestCase. Se você herdar de uma subclasse MyTestCase:

class MyTestCase(unittest.TestCase):
    def assertNotRaises(self, expected_exception, *args, **kwargs):
        context = _AssertNotRaisesContext(expected_exception, self)
        try:
            return context.handle('assertNotRaises', args, kwargs)
        finally:
            context = None

todos os seus casos de teste agora terão o assertNotRaisesmétodo disponível para eles.


De onde vem a tracebacksua elsedeclaração?
NOHS

11
@ NOhs Houve uma falta import. Sua fixa
tel

2
def _assertNotRaises(self, exception, obj, attr):                                                                                                                              
     try:                                                                                                                                                                       
         result = getattr(obj, attr)                                                                                                                                            
         if hasattr(result, '__call__'):                                                                                                                                        
             result()                                                                                                                                                           
     except Exception as e:                                                                                                                                                     
         if isinstance(e, exception):                                                                                                                                           
            raise AssertionError('{}.{} raises {}.'.format(obj, attr, exception)) 

pode ser modificado se você precisar aceitar parâmetros.

ligue como

self._assertNotRaises(IndexError, array, 'sort')

1

Eu achei útil fazer o patch do macaco da unittestseguinte maneira:

def assertMayRaise(self, exception, expr):
  if exception is None:
    try:
      expr()
    except:
      info = sys.exc_info()
      self.fail('%s raised' % repr(info[0]))
  else:
    self.assertRaises(exception, expr)

unittest.TestCase.assertMayRaise = assertMayRaise

Isso esclarece a intenção ao testar a ausência de uma exceção:

self.assertMayRaise(None, does_not_raise)

Isso também simplifica o teste em um loop, o que muitas vezes me vejo fazendo:

# ValueError is raised only for op(x,x), op(y,y) and op(z,z).
for i,(a,b) in enumerate(itertools.product([x,y,z], [x,y,z])):
  self.assertMayRaise(None if i%4 else ValueError, lambda: op(a, b))

O que é um patch de macaco?
21818 ScottMcC

11
Consulte en.wikipedia.org/wiki/Monkey_patch . Depois de adicionar assertMayRaisea unittest.TestSuitevocê pode apenas fingir que é parte da unittestbiblioteca.
21718 AndyJost

0

Se você passar uma classe Exception assertRaises(), é fornecido um gerenciador de contexto. Isso pode melhorar a legibilidade de seus testes:

# raise exception if Application created with bad data
with self.assertRaises(pySourceAidExceptions.PathIsNotAValidOne):
    application = Application("abcdef", "")

Isso permite testar casos de erro no seu código.

Nesse caso, você está testando o PathIsNotAValidOneaumento quando passa parâmetros inválidos para o construtor de aplicativos.


11
Não, isso só falhará se a exceção não for gerada dentro do bloco do gerenciador de contexto. Pode ser facilmente testado por 'with self.assertRaises (TypeError): raise TypeError', que passa.
Matthew Trevor

@MatthewTrevor Good call. Pelo que me lembro, em vez de testar o código executa corretamente, ou seja, não aumenta, eu estava sugerindo testar casos de erro. Eu editei a resposta de acordo. Espero que eu possa ganhar um +1 para sair do vermelho. :)
hiwaylon

Note, este é também Python 2.7 e posterior: docs.python.org/2/library/...
qneill

0

você pode tentar assim. try: self.assertRaises (None, function, arg1, arg2), exceto: pass se você não colocar o código dentro do bloco try, ele passará pela exceção 'AssertionError: None not raise' e o caso de teste falhará. O caso de teste será aprovado se colocado dentro do bloco try, que é o comportamento esperado.


0

Uma maneira direta de garantir que o objeto seja inicializado sem nenhum erro é testar a instância de tipo do objeto.

Aqui está um exemplo :

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
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.