Python unittest - kebalikan dari assertRaises?

374

Saya ingin menulis tes untuk memastikan bahwa Pengecualian tidak dimunculkan dalam keadaan tertentu.

Ini mudah untuk menguji apakah sebuah Exception adalah mengangkat ...

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

... tapi bagaimana Anda bisa melakukan yang sebaliknya .

Sesuatu seperti ini saya apa yang saya cari ...

sValidPath=AlwaysSuppliesAValidPath()
self.assertNotRaises(PathIsNotAValidOne, MyObject, sValidPath) 
glaucon
sumber
6
Anda selalu bisa melakukan apa saja yang seharusnya berhasil dalam ujian. Jika memunculkan kesalahan, itu akan muncul (dihitung sebagai kesalahan, bukan kegagalan). Tentu saja, itu mengasumsikan bahwa itu tidak menimbulkan kesalahan, bukan hanya jenis kesalahan yang ditentukan. Selain itu, saya kira Anda harus menulis sendiri.
Thomas K
Ternyata Anda sebenarnya bisa menerapkan assertNotRaisesmetode yang berbagi 90% dari kode / perilakunya dengan assertRaisessekitar ~ 30-ish baris kode. Lihat jawaban saya di bawah untuk detailnya.
tel.
Saya ingin ini sehingga saya dapat membandingkan dua fungsi dengan hypothesismemastikan mereka menghasilkan output yang sama untuk semua jenis input, sementara mengabaikan kasus-kasus di mana yang asli menimbulkan pengecualian. assume(func(a))tidak berfungsi karena output dapat berupa array dengan nilai kebenaran yang ambigu. Jadi saya hanya ingin memanggil fungsi dan dapatkan Truejika tidak gagal. assume(func(a) is not None)karya saya kira
endolith

Jawaban:

394
def run_test(self):
    try:
        myFunc()
    except ExceptionType:
        self.fail("myFunc() raised ExceptionType unexpectedly!")
Bina Marga
sumber
32
@hiwaylon - Tidak, ini sebenarnya adalah solusi yang tepat. Solusi yang diajukan oleh user9876 secara konseptual cacat: jika Anda menguji untuk tidak meningkatkan katakan ValueError, tetapi ValueErrormalah dinaikkan, tes Anda harus keluar dengan kondisi kegagalan, bukan kesalahan. Di sisi lain, jika dalam menjalankan kode yang sama Anda akan menaikkan KeyError, itu akan menjadi kesalahan, bukan kegagalan. Dalam python - berbeda dari beberapa bahasa lain - Pengecualian secara rutin digunakan untuk aliran kontrol, inilah mengapa kami memiliki except <ExceptionName>sintaks yang sesungguhnya. Untuk itu, solusi user9876 benar-benar salah.
mac
@ Mac - Apakah ini juga solusi yang benar? stackoverflow.com/a/4711722/6648326
MasterJoe2
Yang ini memiliki efek yang disayangkan menunjukkan cakupan <100% (kecuali tidak akan pernah terjadi) untuk tes.
Shay
3
@Shay, IMO Anda harus selalu mengecualikan file tes sendiri dari laporan cakupan (karena mereka hampir selalu berjalan 100% menurut definisi, Anda akan secara artifisial menggembungkan laporan)
Saus BBQ Asli
@ saus asli-bbq, bukankah itu membuat saya terbuka untuk tes yang dilewati secara tidak sengaja. Misalnya, salah ketik nama tes (ttst_function), konfigurasi run salah di pycharm, dll?
Shay
67

Hai - Saya ingin menulis tes untuk memastikan bahwa Pengecualian tidak dimunculkan dalam keadaan tertentu.

Itu asumsi default - pengecualian tidak dimunculkan.

Jika Anda tidak mengatakan apa-apa lagi, itu dianggap dalam setiap tes.

Anda tidak harus benar-benar menulis pernyataan apa pun untuk itu.

S.Lott
sumber
7
@IndradhanushGupta Yah jawaban yang diterima membuat tes lebih pythonic daripada yang ini. Eksplisit lebih baik daripada implisit.
0xc0de
17
Tidak ada komentator lain yang menunjukkan mengapa jawaban ini salah, meskipun itu adalah alasan yang sama dengan jawaban user9876 salah: kegagalan dan kesalahan adalah hal yang berbeda dalam kode uji. Jika fungsi Anda adalah untuk melemparkan pengecualian selama tes yang tidak menegaskan, kerangka uji akan memperlakukan itu sebagai kesalahan , bukan kegagalan untuk tidak menegaskan.
coredumperror
@CoreDumpError Saya memahami perbedaan antara kegagalan dan kesalahan, tetapi tidakkah ini memaksa Anda untuk mengelilingi setiap tes dengan konstruksi coba / pengecualian? Atau apakah Anda merekomendasikan untuk melakukan itu hanya untuk tes yang secara eksplisit meningkatkan pengecualian dalam beberapa kondisi (yang pada dasarnya berarti bahwa pengecualian diharapkan ).
federicojasson
4
@federicojasson Anda menjawab pertanyaan Anda dengan cukup baik di kalimat kedua itu. Kesalahan vs kegagalan dalam pengujian dapat secara ringkas dijelaskan sebagai "crash tak terduga" vs "perilaku yang tidak diinginkan", masing-masing. Anda ingin pengujian Anda menunjukkan kesalahan saat fungsi Anda macet, tetapi tidak ketika pengecualian yang Anda tahu akan dilempar karena input tertentu dilemparkan ketika diberi input berbeda.
coredumperror
52

Panggil saja fungsinya. Jika memunculkan pengecualian, kerangka kerja unit test akan menandai ini sebagai kesalahan. Anda mungkin ingin menambahkan komentar, misalnya:

sValidPath=AlwaysSuppliesAValidPath()
# Check PathIsNotAValidOne not thrown
MyObject(sValidPath)
pengguna9876
sumber
35
Kegagalan dan Kesalahan secara konseptual berbeda. Selain itu, karena dalam python, Pengecualian secara rutin digunakan untuk aliran kontrol, ini akan membuat sangat sulit untuk dipahami sekilas (= tanpa menjelajahi kode tes) jika Anda telah melanggar logika atau kode Anda ...
mac
1
Entah tes Anda lulus atau tidak. Jika tidak lulus, Anda harus memperbaikinya. Apakah itu dilaporkan sebagai "kegagalan" atau "kesalahan" sebagian besar tidak relevan. Ada satu perbedaan: dengan jawaban saya Anda akan melihat jejak tumpukan sehingga Anda dapat melihat di mana PathIsNotAValidOne dilemparkan; dengan jawaban yang diterima Anda tidak akan memiliki informasi itu sehingga debugging akan lebih sulit. (Dengan asumsi Py2; tidak yakin apakah Py3 lebih baik dalam hal ini).
user9876
19
@ user9876 - Tidak. Kondisi keluar tes adalah 3 (lulus / nopass / kesalahan), bukan 2 karena Anda tampaknya salah percaya. Perbedaan antara kesalahan dan kegagalan adalah substansial dan memperlakukan mereka seolah-olah sama saja dengan pemrograman yang buruk. Jika Anda tidak mempercayai saya, lihat saja bagaimana uji pelari bekerja dan pohon keputusan apa yang mereka implementasikan untuk kegagalan dan kesalahan. Sebuah titik yang baik mulai untuk python adalah yang xfaildekorator di pytest.
mac
4
Saya kira itu tergantung pada bagaimana Anda menggunakan unit test. Cara tim saya menggunakan unit test, semuanya harus lulus. (Pemrograman Agile, dengan mesin integrasi berkelanjutan yang menjalankan semua tes unit). Saya tahu bahwa test case dapat melaporkan "lulus", "gagal" atau "kesalahan". Tetapi pada tingkat tinggi, apa yang sebenarnya penting bagi tim saya adalah "apakah semua tes unit lulus?" (yaitu "apakah Jenkins hijau?"). Jadi untuk tim saya, tidak ada perbedaan praktis antara "gagal" dan "kesalahan". Anda mungkin memiliki persyaratan yang berbeda jika menggunakan tes unit dengan cara yang berbeda.
user9876
1
@ user9876 perbedaan antara 'gagal' dan 'kesalahan' adalah perbedaan antara "pernyataan saya gagal" dan "tes saya bahkan tidak sampai ke pernyataan". Bagi saya, itu adalah perbedaan yang berguna selama memperbaiki tes, tapi saya kira, seperti yang Anda katakan, tidak untuk semua orang.
CS
14

Saya poster asli dan saya menerima jawaban di atas oleh DGH tanpa terlebih dahulu menggunakannya dalam kode.

Setelah saya menggunakan saya menyadari bahwa perlu sedikit penyesuaian untuk benar-benar melakukan apa yang saya butuhkan (untuk bersikap adil kepada DJBM, dia memang mengatakan "atau sesuatu yang serupa"!).

Saya pikir layak memposting tweak di sini untuk kepentingan orang lain:

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

Apa yang saya coba lakukan di sini adalah untuk memastikan bahwa jika upaya dilakukan untuk membuat instance objek Aplikasi dengan argumen spasi kedua, pySourceAidExceptions.PathIsNotAValidOne akan dimunculkan.

Saya percaya bahwa menggunakan kode di atas (sangat bergantung pada jawaban DJBM) akan melakukannya.

glaucon
sumber
2
Karena Anda mengklarifikasi pertanyaan Anda dan tidak menjawabnya, Anda harus mengeditnya (tidak menjawabnya). Silakan lihat jawaban saya di bawah ini.
hiwaylon
13
Tampaknya sangat berlawanan dengan masalah aslinya. self.assertRaises(PathIsNotAValidOne, MyObject, sInvalidPath)harus melakukan pekerjaan dalam kasus ini.
Antony Hatchkins
8

Anda dapat mendefinisikan assertNotRaisesdengan menggunakan kembali sekitar 90% dari implementasi asli assertRaisesdalam unittestmodul. Dengan pendekatan ini, Anda berakhir dengan assertNotRaisesmetode yang, selain dari kondisi kegagalannya yang terbalik, juga berperilaku identik assertRaises.

TLDR dan demo langsung

Ternyata sangat mudah untuk menambahkan assertNotRaisesmetode unittest.TestCase(butuh saya sekitar 4 kali lebih lama untuk menulis jawaban ini seperti halnya kode). Berikut ini demo langsung dari assertNotRaisesmetode yang digunakan . Sama sepertiassertRaises , Anda bisa menyampaikan callable dan args ke assertNotRaises, atau Anda dapat menggunakannya dalam sebuah withpernyataan. Demo langsung mencakup uji kasus yang menunjukkan bahwa assertNotRaisesberfungsi sebagaimana dimaksud.

Detail

Implementasi assertRaisesdalam unittestcukup rumit, tetapi dengan sedikit subclass pintar Anda dapat menimpa dan membalikkan kondisi kegagalannya.

assertRaisesadalah metode singkat yang pada dasarnya hanya membuat instance dari unittest.case._AssertRaisesContextkelas dan mengembalikannya (lihat definisi dalam unittest.casemodul). Anda dapat mendefinisikan _AssertNotRaisesContextkelas Anda sendiri dengan mensubclassing _AssertRaisesContextdan menimpa __exit__metodenya:

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

Biasanya Anda mendefinisikan kelas kasus uji dengan meminta mereka mewarisi dari TestCase. Jika Anda sebaliknya mewarisi dari subkelas 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

semua kasus uji Anda sekarang akan memiliki assertNotRaisesmetode yang tersedia untuk mereka.

tel
sumber
Dari mana asal pernyataan tracebackAnda else?
Nggak
1
@NOh Ada yang hilang import. Sudah diperbaiki
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)) 

dapat dimodifikasi jika Anda perlu menerima parameter.

panggilan seperti

self._assertNotRaises(IndexError, array, 'sort')
znotdead
sumber
1

Saya merasa bermanfaat untuk menambal monyet unittestsebagai berikut:

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

Ini mengklarifikasi maksud saat menguji untuk tidak adanya pengecualian:

self.assertMayRaise(None, does_not_raise)

Ini juga menyederhanakan pengujian dalam satu lingkaran, yang sering saya lakukan:

# 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))
AndyJost
sumber
Apa itu tambalan-monyet?
ScottMcC
1
Lihat en.wikipedia.org/wiki/Monkey_patch . Setelah menambahkan assertMayRaiseuntuk unittest.TestSuiteAnda hanya bisa berpura-pura bagian itu dari unittestperpustakaan.
AndyJost
0

Jika Anda lulus kelas Pengecualian assertRaises(), manajer konteks disediakan. Ini dapat meningkatkan keterbacaan tes Anda:

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

Ini memungkinkan Anda untuk menguji kasus kesalahan dalam kode Anda.

Dalam hal ini, Anda menguji PathIsNotAValidOnedinaikkan ketika Anda melewati parameter yang tidak valid ke konstruktor Aplikasi.

hiwaylon
sumber
1
Tidak, ini hanya akan gagal jika pengecualian tidak dimunculkan dalam blok manajer konteks. Dapat dengan mudah diuji oleh 'dengan self.assertRaises (TypeError): naikkan TypeError', yang lewat.
Matthew Trevor
@ MatthewTrevor Panggilan bagus. Seingat saya, alih-alih pengujian kode dijalankan dengan benar, yaitu tidak naik, saya menyarankan pengujian kasus kesalahan. Saya telah mengedit jawaban yang sesuai. Semoga saya bisa mendapatkan +1 untuk keluar dari zona merah. :)
hiwaylon
Catatan, ini juga Python 2.7 dan yang lebih baru: docs.python.org/2/library/…
qneill
0

Anda bisa coba seperti itu. coba: self.assertRaises (Tidak ada, fungsi, arg1, arg2) kecuali: lulus jika Anda tidak memasukkan kode di dalam blok coba itu akan melalui pengecualian 'AssertionError: Tidak ada yang mengangkat "dan test case akan gagal. Test case akan lulus. Test case akan lulus jika diletakkan di dalam blok coba yang merupakan perilaku yang diharapkan.

lalit
sumber
0

Salah satu cara lurus ke depan untuk memastikan objek diinisialisasi tanpa kesalahan adalah dengan menguji instance tipe objek.

Berikut ini sebuah contoh:

p = SomeClass(param1=_param1_value)
self.assertTrue(isinstance(p, SomeClass))
anroyus
sumber