Mengeluarkan data dari unit test dengan python

115

Jika saya menulis pengujian unit dalam python (menggunakan modul unittest), apakah mungkin untuk mengeluarkan data dari pengujian yang gagal, jadi saya dapat memeriksanya untuk membantu menyimpulkan apa yang menyebabkan kesalahan? Saya menyadari kemampuan untuk membuat pesan yang disesuaikan, yang dapat membawa beberapa informasi, tetapi terkadang Anda mungkin berurusan dengan data yang lebih kompleks, yang tidak dapat dengan mudah direpresentasikan sebagai string.

Misalnya, Anda memiliki kelas Foo, dan sedang menguji bilah metode, menggunakan data dari daftar yang disebut testdata:

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

Jika tes gagal, saya mungkin ingin mengeluarkan t1, t2 dan / atau f, untuk melihat mengapa data khusus ini mengakibatkan kegagalan. Dengan output, yang saya maksud adalah variabel dapat diakses seperti variabel lainnya, setelah pengujian dijalankan.

Gegat
sumber

Jawaban:

73

Jawaban yang sangat terlambat untuk seseorang yang, seperti saya, datang ke sini mencari jawaban yang sederhana dan cepat.

Di Python 2.7 Anda dapat menggunakan parameter tambahan msguntuk menambahkan informasi ke pesan kesalahan seperti ini:

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

Dokumen resmi di sini

Facundo Casco
sumber
1
Bekerja dengan Python 3 juga.
MrDBA
18
Dokumen mengisyaratkan hal ini tetapi perlu disebutkan secara eksplisit: secara default, jika msgdigunakan, ini akan menggantikan pesan kesalahan normal. Untuk msgditambahkan ke pesan kesalahan normal, Anda juga perlu menyetel TestCase.longMessage ke True
Catalin Iacob
1
baik untuk mengetahui bahwa kita dapat mengirimkan pesan kesalahan khusus, tetapi saya tertarik untuk mencetak beberapa pesan terlepas dari kesalahannya.
Harry Moreno
5
Komentar oleh @CatalinIacob berlaku untuk Python 2.x. Di Python 3.x, TestCase.longMessage secara default adalah True.
ndmeiri
70

Kami menggunakan modul logging untuk ini.

Sebagai contoh:

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()

Itu memungkinkan kita untuk mengaktifkan debugging untuk pengujian tertentu yang kita tahu gagal dan yang kita inginkan informasi debug tambahannya.

Metode pilihan saya, bagaimanapun, bukanlah menghabiskan banyak waktu untuk debugging, tetapi menghabiskannya menulis tes yang lebih terperinci untuk mengungkap masalah.

S. Lott
sumber
Bagaimana jika saya memanggil metode foo di dalam testSomething dan itu mencatat sesuatu. Bagaimana saya bisa melihat keluaran untuk itu tanpa melewatkan logger ke foo?
simao
@simao: Apa itu foo? Fungsi terpisah? Fungsi metode SomeTest? Dalam kasus pertama, suatu fungsi dapat memiliki pencatatnya sendiri. Dalam kasus kedua, fungsi metode lain dapat memiliki pencatatnya sendiri. Apakah Anda mengetahui cara kerja loggingpaket? Banyak penebang adalah norma.
S. Lotot
8
Saya menyiapkan logging persis seperti yang Anda tentukan. Saya berasumsi ini berfungsi, tetapi di mana saya dapat melihat hasilnya? Ini tidak menghasilkan keluaran ke konsol. Saya mencoba mengonfigurasinya dengan masuk ke file, tetapi itu juga tidak menghasilkan output apa pun.
MikeyE
"Metode yang saya sukai, bagaimanapun, bukanlah menghabiskan banyak waktu untuk debugging, tetapi menghabiskannya menulis tes yang lebih terperinci untuk mengungkap masalah." - kata yang bagus!
Seth
34

Anda dapat menggunakan pernyataan cetak sederhana, atau cara penulisan lainnya untuk stdout. Anda juga dapat menjalankan debugger Python di mana saja dalam pengujian Anda.

Jika Anda menggunakan nose untuk menjalankan pengujian (yang saya rekomendasikan), ini akan mengumpulkan stdout untuk setiap pengujian dan hanya menampilkannya kepada Anda jika pengujian gagal, jadi Anda tidak harus hidup dengan output yang berantakan saat pengujian berhasil.

nose juga memiliki sakelar untuk secara otomatis menampilkan variabel yang disebutkan dalam asserts, atau untuk memanggil debugger pada pengujian yang gagal. Misalnya -s( --nocapture) mencegah penangkapan stdout.

Ned Batchelder
sumber
Sayangnya, nose sepertinya tidak mengumpulkan log yang ditulis ke stdout / err menggunakan framework logging. Saya memiliki printdan di log.debug()samping satu sama lain, dan secara eksplisit mengaktifkan DEBUGlogging di root dari setUp()metode ini, tetapi hanya printoutput yang muncul.
haridsv
7
nosetests -smenunjukkan isi stdout apakah ada kesalahan atau tidak - sesuatu yang menurut saya berguna.
hargriffle
Saya tidak dapat menemukan sakelar untuk secara otomatis menampilkan variabel di dokumen hidung. Dapatkah Anda menunjukkan kepada saya sesuatu yang menggambarkan mereka?
ABM
Saya tidak tahu cara untuk menampilkan variabel secara otomatis dari nose atau unittest. Saya mencetak hal-hal yang ingin saya lihat dalam pengujian saya.
Ned Batchelder
16

Menurut saya ini bukan yang Anda cari, tidak ada cara untuk menampilkan nilai variabel yang tidak gagal, tetapi ini dapat membantu Anda lebih dekat untuk mengeluarkan hasil seperti yang Anda inginkan.

Anda bisa menggunakan objek TestResult yang dikembalikan oleh TestRunner.run () untuk analisis dan pemrosesan hasil. Khususnya, TestResult.errors dan TestResult.failures

Tentang Objek TestResults:

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

Dan beberapa kode untuk mengarahkan Anda ke arah yang benar:

>>> 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
sumber
5

Opsi lain - mulai debugger di mana pengujian gagal.

Coba jalankan pengujian Anda dengan Testoob (ini akan menjalankan rangkaian unittest Anda tanpa perubahan), dan Anda dapat menggunakan sakelar baris perintah '--debug' untuk membuka debugger saat pengujian gagal.

Berikut sesi terminal di 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
sumber
2
Nose ( nose.readthedocs.org/en/latest/index.html ) adalah kerangka kerja lain yang menyediakan opsi 'mulai sesi debugger'. Saya menjalankannya dengan '-sx --pdb --pdb-failures', yang tidak memakan output, berhenti setelah kegagalan pertama, dan turun ke pdb pada pengecualian dan kegagalan pengujian. Ini telah menghilangkan kebutuhan saya akan pesan kesalahan yang kaya, kecuali saya malas dan menguji dalam satu putaran.
jwhitlock
5

Metode yang saya gunakan sangat sederhana. Saya hanya mencatatnya sebagai peringatan agar benar-benar muncul.

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
sumber
Apakah ini akan berhasil jika tes berhasil? Dalam kasus saya, peringatan hanya ditampilkan jika tes gagal
Shreya Maria
@Shreya ya itu akan
Orane
5

Saya pikir saya mungkin terlalu memikirkan hal ini. Salah satu cara saya menemukan yang melakukan pekerjaan itu, cukup dengan memiliki variabel global, yang mengakumulasi data diagnostik.

Sesuatu seperti ini:

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")

Terima kasih atas balasannya. Mereka telah memberi saya beberapa ide alternatif tentang cara merekam informasi dari pengujian unit dengan python.

Gegat
sumber
2

Gunakan logging:

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

Pemakaian:

# 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

Jika Anda tidak mengatur LOG_FILE, logging harus stderr.

bukan-pengguna
sumber
2

Anda dapat menggunakan loggingmodul untuk itu.

Jadi dalam kode unit test, gunakan:

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.")

Secara default, peringatan dan error di-output ke /dev/stderr, jadi harus terlihat di konsol.

Untuk menyesuaikan log (seperti pemformatan), coba contoh berikut:

# 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
sumber
2

Apa yang saya lakukan dalam kasus ini adalah memiliki log.debug()dengan beberapa pesan di aplikasi saya. Karena level logging default adalah WARNING, pesan seperti itu tidak ditampilkan dalam eksekusi normal.

Kemudian, di unittest saya mengubah level logging ke DEBUG, sehingga pesan seperti itu ditampilkan saat menjalankannya.

import logging

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

Di unittests:

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



Lihat contoh lengkapnya:

Ini adalah daikiri.pykelas dasar yang mengimplementasikan Daikiri dengan nama dan harganya. Ada metode make_discount()yang mengembalikan harga daikiri tertentu setelah menerapkan diskon yang diberikan:

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

Kemudian, saya membuat unittest test_daikiri.pyyang memeriksa penggunaannya:

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()

Jadi ketika saya menjalankannya, saya mendapatkan log.debugpesan:

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

OK
fedorqui 'JADI berhenti merugikan'
sumber
1

inspect.trace akan membiarkan Anda mendapatkan variabel lokal setelah pengecualian diberikan. Anda kemudian dapat membungkus tes unit dengan dekorator seperti yang berikut untuk menyimpan variabel lokal tersebut untuk diperiksa selama pemeriksaan mayat.

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))

Baris terakhir akan mencetak nilai yang dikembalikan di mana tes berhasil dan variabel lokal, dalam hal ini x, ketika gagal:

{'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
sumber
0

Bagaimana menangkap pengecualian yang dihasilkan dari kegagalan pernyataan? Di blok tangkapan Anda, Anda dapat mengeluarkan data sesuka Anda ke mana pun. Kemudian setelah selesai, Anda dapat membuang kembali pengecualian tersebut. Pelari tes mungkin tidak akan tahu bedanya.

Penafian: Saya belum mencoba ini dengan kerangka pengujian unit python tetapi dengan kerangka pengujian unit lainnya.

Sam Corder
sumber
-1

Memperluas jawaban @FC, ini bekerja dengan cukup baik untuk saya:

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
sumber