Saída de dados de teste de unidade em python

115

Se estou escrevendo testes de unidade em python (usando o módulo unittest), é possível gerar dados de um teste que falhou, para que eu possa examiná-lo para ajudar a deduzir o que causou o erro? Estou ciente da capacidade de criar uma mensagem personalizada, que pode conter algumas informações, mas às vezes você pode lidar com dados mais complexos, que não podem ser facilmente representados como uma string.

Por exemplo, suponha que você tenha uma classe Foo e esteja testando uma barra de método, usando dados de uma lista chamada testdata:

class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1)
            self.assertEqual(f.bar(t2), 2)

Se o teste falhou, eu poderia querer produzir t1, t2 e / ou f, para ver por que esses dados específicos resultaram em uma falha. Por saída, quero dizer que as variáveis ​​podem ser acessadas como quaisquer outras variáveis, após o teste ter sido executado.

Silverfish
fonte

Respostas:

73

Resposta muito tardia para quem, como eu, chega aqui procurando uma resposta simples e rápida.

No Python 2.7, você pode usar um parâmetro msgadicional para adicionar informações à mensagem de erro como esta:

self.assertEqual(f.bar(t2), 2, msg='{0}, {1}'.format(t1, t2))

Documentos oficiais aqui

Facundo Casco
fonte
1
Funciona em Python 3 também.
MrDBA de
18
A documentação sugere isso, mas vale a pena mencionar explicitamente: por padrão, se msgfor usado, substituirá a mensagem de erro normal. Para ser msganexado à mensagem de erro normal, você também precisa definir TestCase.longMessage como True
Catalin Iacob
1
bom saber que podemos passar uma mensagem de erro personalizada, mas estou interessado em imprimir alguma mensagem independente do erro.
Harry Moreno de
5
O comentário de @CatalinIacob se aplica ao Python 2.x. No Python 3.x, TestCase.longMessage é padronizado como True.
ndmeiri
70

Usamos o módulo de registro para isso.

Por exemplo:

import logging
class SomeTest( unittest.TestCase ):
    def testSomething( self ):
        log= logging.getLogger( "SomeTest.testSomething" )
        log.debug( "this= %r", self.this )
        log.debug( "that= %r", self.that )
        # etc.
        self.assertEquals( 3.14, pi )

if __name__ == "__main__":
    logging.basicConfig( stream=sys.stderr )
    logging.getLogger( "SomeTest.testSomething" ).setLevel( logging.DEBUG )
    unittest.main()

Isso nos permite ativar a depuração para testes específicos que sabemos que estão falhando e para os quais desejamos informações adicionais de depuração.

Meu método preferido, entretanto, não é gastar muito tempo depurando, mas escrevendo testes mais refinados para expor o problema.

S.Lott
fonte
E se eu chamar um método foo dentro de testSomething e ele registrar algo. Como posso ver a saída disso sem passar o logger para foo?
simo
@simao: O que é foo? Uma função separada? Uma função de método de SomeTest? No primeiro caso, uma função pode ter seu próprio logger. No segundo caso, a outra função do método pode ter seu próprio logger. Você sabe como o loggingpacote funciona? Vários loggers é a norma.
S.Lott
8
Eu configurei o registro da maneira exata que você especificou. Presumo que esteja funcionando, mas onde vejo a saída? Não está enviando para o console. Tentei configurá-lo com registro em um arquivo, mas também não produziu nenhuma saída.
MikeyE
"Meu método preferido, no entanto, não é gastar muito tempo depurando, mas escrevendo testes mais refinados para expor o problema." -- bem dito!
Seth
34

Você pode usar instruções de impressão simples ou qualquer outra maneira de escrever no stdout. Você também pode chamar o depurador Python em qualquer lugar em seus testes.

Se você usa nariz para executar seus testes (o que eu recomendo), ele coletará o stdout para cada teste e apenas o mostrará se o teste falhar, para que você não tenha que conviver com a saída desordenada quando os testes passarem.

O nariz também tem opções para mostrar automaticamente as variáveis ​​mencionadas em declarações ou para chamar o depurador em testes com falha. Por exemplo -s( --nocapture) impede a captura de stdout.

Ned Batchelder
fonte
Infelizmente, o nose não parece coletar o log escrito em stdout / err usando a estrutura de log. Eu tenho o printe log.debug()um ao lado do outro, e habilito explicitamente o DEBUGregistro na raiz do setUp()método, mas apenas a printsaída é exibida.
haridsv
7
nosetests -smostra o conteúdo de stdout se houver um erro ou não - algo que considero útil.
hargriffle
Não consigo encontrar os interruptores para mostrar variáveis ​​automaticamente nos documentos do nariz. Você pode me apontar algo que os descreva?
ABM
Não conheço uma maneira de mostrar automaticamente as variáveis ​​do nariz ou do teste de unidade. Eu imprimo as coisas que quero ver nos meus testes.
Ned Batchelder
16

Não acho que seja exatamente isso que você está procurando, não há como exibir valores de variáveis ​​que não falhem, mas isso pode ajudá-lo a chegar mais perto de produzir os resultados da maneira que deseja.

Você pode usar o objeto TestResult retornado pelo TestRunner.run () para análise e processamento de resultados. Particularmente, TestResult.errors e TestResult.failures

Sobre o objeto TestResults:

http://docs.python.org/library/unittest.html#id3

E algum código para apontar a direção certa:

>>> import random
>>> import unittest
>>>
>>> class TestSequenceFunctions(unittest.TestCase):
...     def setUp(self):
...         self.seq = range(5)
...     def testshuffle(self):
...         # make sure the shuffled sequence does not lose any elements
...         random.shuffle(self.seq)
...         self.seq.sort()
...         self.assertEqual(self.seq, range(10))
...     def testchoice(self):
...         element = random.choice(self.seq)
...         error_test = 1/0
...         self.assert_(element in self.seq)
...     def testsample(self):
...         self.assertRaises(ValueError, random.sample, self.seq, 20)
...         for element in random.sample(self.seq, 5):
...             self.assert_(element in self.seq)
...
>>> suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
>>> testResult = unittest.TextTestRunner(verbosity=2).run(suite)
testchoice (__main__.TestSequenceFunctions) ... ERROR
testsample (__main__.TestSequenceFunctions) ... ok
testshuffle (__main__.TestSequenceFunctions) ... FAIL

======================================================================
ERROR: testchoice (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 11, in testchoice
ZeroDivisionError: integer division or modulo by zero

======================================================================
FAIL: testshuffle (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "<stdin>", line 8, in testshuffle
AssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

----------------------------------------------------------------------
Ran 3 tests in 0.031s

FAILED (failures=1, errors=1)
>>>
>>> testResult.errors
[(<__main__.TestSequenceFunctions testMethod=testchoice>, 'Traceback (most recent call last):\n  File "<stdin>"
, line 11, in testchoice\nZeroDivisionError: integer division or modulo by zero\n')]
>>>
>>> testResult.failures
[(<__main__.TestSequenceFunctions testMethod=testshuffle>, 'Traceback (most recent call last):\n  File "<stdin>
", line 8, in testshuffle\nAssertionError: [0, 1, 2, 3, 4] != [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]\n')]
>>>
Monkut
fonte
5

Outra opção - inicie um depurador onde o teste falhar.

Tente executar seus testes com Testoob (ele executará seu pacote de teste de unidade sem alterações), e você pode usar a opção de linha de comando '--debug' para abrir um depurador quando um teste falhar.

Esta é uma sessão de terminal no Windows:

C:\work> testoob tests.py --debug
F
Debugging for failure in test: test_foo (tests.MyTests.test_foo)
> c:\python25\lib\unittest.py(334)failUnlessEqual()
-> (msg or '%r != %r' % (first, second))
(Pdb) up
> c:\work\tests.py(6)test_foo()
-> self.assertEqual(x, y)
(Pdb) l
  1     from unittest import TestCase
  2     class MyTests(TestCase):
  3       def test_foo(self):
  4         x = 1
  5         y = 2
  6  ->     self.assertEqual(x, y)
[EOF]
(Pdb)
orip
fonte
2
Nose ( nose.readthedocs.org/en/latest/index.html ) é outra estrutura que fornece opções para 'iniciar uma sessão de depurador'. Eu o executo com '-sx --pdb --pdb-failures', que não consome a saída, para após a primeira falha e cai no pdb em exceções e falhas de teste. Isso removeu minha necessidade de mensagens de erro ricas, a menos que eu seja preguiçoso e esteja testando em um loop.
jwhitlock
5

O método que uso é muito simples. Eu apenas registro como um aviso para que ele realmente apareça.

import logging

class TestBar(unittest.TestCase):
    def runTest(self):

       #this line is important
       logging.basicConfig()
       log = logging.getLogger("LOG")

       for t1, t2 in testdata:
         f = Foo(t1)
         self.assertEqual(f.bar(t2), 2)
         log.warning(t1)
Orane
fonte
Isso funcionará se o teste for bem-sucedido? No meu caso, o aviso é mostrado apenas se o teste falhar
Shreya Maria
@ShreyaMaria sim, vai
Orane
5

Acho que estou pensando demais nisso. Uma maneira que descobri para fazer o trabalho é simplesmente ter uma variável global, que acumula os dados de diagnóstico.

Algo assim:

log1 = dict()
class TestBar(unittest.TestCase):
    def runTest(self):
        for t1, t2 in testdata:
            f = Foo(t1) 
            if f.bar(t2) != 2: 
                log1("TestBar.runTest") = (f, t1, t2)
                self.fail("f.bar(t2) != 2")

Obrigado pelas respostas. Eles me deram algumas idéias alternativas sobre como registrar informações de testes de unidade em python.

Silverfish
fonte
2

Use o registro:

import unittest
import logging
import inspect
import os

logging_level = logging.INFO

try:
    log_file = os.environ["LOG_FILE"]
except KeyError:
    log_file = None

def logger(stack=None):
    if not hasattr(logger, "initialized"):
        logging.basicConfig(filename=log_file, level=logging_level)
        logger.initialized = True
    if not stack:
        stack = inspect.stack()
    name = stack[1][3]
    try:
        name = stack[1][0].f_locals["self"].__class__.__name__ + "." + name
    except KeyError:
        pass
    return logging.getLogger(name)

def todo(msg):
    logger(inspect.stack()).warning("TODO: {}".format(msg))

def get_pi():
    logger().info("sorry, I know only three digits")
    return 3.14

class Test(unittest.TestCase):

    def testName(self):
        todo("use a better get_pi")
        pi = get_pi()
        logger().info("pi = {}".format(pi))
        todo("check more digits in pi")
        self.assertAlmostEqual(pi, 3.14)
        logger().debug("end of this test")
        pass

Uso:

# LOG_FILE=/tmp/log python3 -m unittest LoggerDemo
.
----------------------------------------------------------------------
Ran 1 test in 0.047s

OK
# cat /tmp/log
WARNING:Test.testName:TODO: use a better get_pi
INFO:get_pi:sorry, I know only three digits
INFO:Test.testName:pi = 3.14
WARNING:Test.testName:TODO: check more digits in pi

Se você não definir LOG_FILE, o registro terá que ser feito stderr.

não um usuário
fonte
2

Você pode usar logging módulo para isso.

Portanto, no código de teste de unidade, use:

import logging as log

def test_foo(self):
    log.debug("Some debug message.")
    log.info("Some info message.")
    log.warning("Some warning message.")
    log.error("Some error message.")

Por padrão, avisos e erros são enviados para /dev/stderr , portanto, devem estar visíveis no console.

Para personalizar registros (como formatação), tente o seguinte exemplo:

# Set-up logger
if args.verbose or args.debug:
    logging.basicConfig( stream=sys.stdout )
    root = logging.getLogger()
    root.setLevel(logging.INFO if args.verbose else logging.DEBUG)
    ch = logging.StreamHandler(sys.stdout)
    ch.setLevel(logging.INFO if args.verbose else logging.DEBUG)
    ch.setFormatter(logging.Formatter('%(asctime)s %(levelname)s: %(name)s: %(message)s'))
    root.addHandler(ch)
else:
    logging.basicConfig(stream=sys.stderr)
Kenorb
fonte
2

O que eu faço nesses casos é ter um log.debug()com algumas mensagens em meu aplicativo. Uma vez que o nível de registro padrão éWARNING , essas mensagens não aparecem na execução normal.

Então, no teste de unidade, altero o nível de registro para DEBUG, de modo que tais mensagens sejam mostradas durante a execução.

import logging

log.debug("Some messages to be shown just when debugging or unittesting")

Nos testes de unidade:

# Set log level
loglevel = logging.DEBUG
logging.basicConfig(level=loglevel)



Veja um exemplo completo:

daikiri.pyOu seja , uma classe básica que implementa um Daikiri com seu nome e preço. Existe um método make_discount()que retorna o preço daquele daikiri específico após a aplicação de um determinado desconto:

import logging

log = logging.getLogger(__name__)

class Daikiri(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def make_discount(self, percentage):
        log.debug("Deducting discount...")  # I want to see this message
        return self.price * percentage

Então, eu crio um teste de unidade test_daikiri.pyque verifica seu uso:

import unittest
import logging
from .daikiri import Daikiri


class TestDaikiri(unittest.TestCase):
    def setUp(self):
        # Changing log level to DEBUG
        loglevel = logging.DEBUG
        logging.basicConfig(level=loglevel)

        self.mydaikiri = Daikiri("cuban", 25)

    def test_drop_price(self):
        new_price = self.mydaikiri.make_discount(0)
        self.assertEqual(new_price, 0)

if __name__ == "__main__":
    unittest.main()

Então, quando eu o executo, recebo as log.debugmensagens:

$ python -m test_daikiri
DEBUG:daikiri:Deducting discount...
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
fedorqui 'então pare de prejudicar'
fonte
1

inspect.trace permitirá que você obtenha variáveis ​​locais depois que uma exceção for lançada. Você pode então envolver os testes de unidade com um decorador como o seguinte para salvar essas variáveis ​​locais para exame durante a autópsia.

import random
import unittest
import inspect


def store_result(f):
    """
    Store the results of a test
    On success, store the return value.
    On failure, store the local variables where the exception was thrown.
    """
    def wrapped(self):
        if 'results' not in self.__dict__:
            self.results = {}
        # If a test throws an exception, store local variables in results:
        try:
            result = f(self)
        except Exception as e:
            self.results[f.__name__] = {'success':False, 'locals':inspect.trace()[-1][0].f_locals}
            raise e
        self.results[f.__name__] = {'success':True, 'result':result}
        return result
    return wrapped

def suite_results(suite):
    """
    Get all the results from a test suite
    """
    ans = {}
    for test in suite:
        if 'results' in test.__dict__:
            ans.update(test.results)
    return ans

# Example:
class TestSequenceFunctions(unittest.TestCase):

    def setUp(self):
        self.seq = range(10)

    @store_result
    def test_shuffle(self):
        # make sure the shuffled sequence does not lose any elements
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))
        # should raise an exception for an immutable sequence
        self.assertRaises(TypeError, random.shuffle, (1,2,3))
        return {1:2}

    @store_result
    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)
        return {7:2}

    @store_result
    def test_sample(self):
        x = 799
        with self.assertRaises(ValueError):
            random.sample(self.seq, 20)
        for element in random.sample(self.seq, 5):
            self.assertTrue(element in self.seq)
        return {1:99999}


suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
unittest.TextTestRunner(verbosity=2).run(suite)

from pprint import pprint
pprint(suite_results(suite))

A última linha imprimirá os valores retornados onde o teste foi bem-sucedido e as variáveis ​​locais, neste caso x, quando ele falhar:

{'test_choice': {'result': {7: 2}, 'success': True},
 'test_sample': {'locals': {'self': <__main__.TestSequenceFunctions testMethod=test_sample>,
                            'x': 799},
                 'success': False},
 'test_shuffle': {'result': {1: 2}, 'success': True}}

Har det gøy :-)

Max Murphy
fonte
0

Que tal capturar a exceção gerada a partir da falha de asserção? Em seu bloco de captura, você pode enviar os dados da maneira que quiser, em qualquer lugar. Então, quando você terminar, poderá lançar novamente a exceção. O executor de teste provavelmente não saberia a diferença.

Isenção de responsabilidade: eu não tentei isso com a estrutura de teste de unidade do Python, mas tentei com outras estruturas de teste de unidade.

Sam Corder
fonte
-1

Expandindo a resposta de @FC, isso funciona muito bem para mim:

class MyTest(unittest.TestCase):
    def messenger(self, message):
        try:
            self.assertEqual(1, 2, msg=message)
        except AssertionError as e:      
            print "\nMESSENGER OUTPUT: %s" % str(e),
georgepsarakis
fonte