Uji unit python dengan basis dan sub kelas


149

Saat ini saya memiliki beberapa unit test yang berbagi serangkaian tes umum. Ini sebuah contoh:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

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

Output di atas adalah:

Calling BaseTest:testCommon
.Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK

Apakah ada cara untuk menulis ulang di atas sehingga yang pertama testCommontidak dipanggil?

EDIT: Alih-alih menjalankan 5 tes di atas, saya ingin hanya menjalankan 4 tes, 2 dari SubTest1 dan 2 dari SubTest2. Tampaknya Python unittest menjalankan BaseTest sendiri dan saya perlu mekanisme untuk mencegah hal itu terjadi.


Saya melihat belum ada yang menyebutkannya, tetapi apakah Anda memiliki opsi untuk mengubah bagian utama dan menjalankan suite uji yang memiliki semua subclass dari BaseTest?
kon psych

Jawaban:


154

Gunakan multiple inheritance, jadi kelas Anda dengan tes umum tidak dengan sendirinya mewarisi dari TestCase.

import unittest

class CommonTests(object):
    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(unittest.TestCase, CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(unittest.TestCase, CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

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

1
Itu solusi paling elegan sejauh ini.
Thierry Lam

27
Metode ini hanya berfungsi untuk metode setUp dan tearDown jika Anda membalik urutan kelas dasar. Karena metode didefinisikan di unittest.TestCase, dan mereka tidak memanggil super (), maka metode setUp dan tearDown di CommonTests harus menjadi yang pertama di MRO, atau mereka tidak akan dipanggil sama sekali.
Ian Clelland

32
Hanya untuk memperjelas ucapan Ian Clelland sehingga akan lebih jelas bagi orang-orang seperti saya: jika Anda menambahkan setUpdan tearDownmetode ke CommonTestskelas, dan Anda ingin mereka dipanggil untuk setiap tes di kelas turunan, Anda harus membalik urutan kelas dasar, sehingga akan: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

6
Saya bukan penggemar pendekatan ini. Ini menetapkan kontrak dalam kode yang harus diwarisi oleh kelas dari keduanya unittest.TestCase dan CommonTests . Saya pikir setUpClassmetode di bawah ini adalah yang terbaik dan kurang rentan terhadap kesalahan manusia. Entah itu atau membungkus kelas BaseTest dalam kelas wadah yang sedikit lebih hacky tetapi menghindari pesan lewati dalam cetakan uji coba.
David Sanders

10
Masalah dengan yang satu ini adalah pylint cocok karena CommonTestsmemanggil metode yang tidak ada di kelas itu.
MadScientist

146

Jangan gunakan banyak pewarisan, itu akan menggigit Anda nanti .

Sebagai gantinya, Anda bisa memindahkan kelas dasar ke modul terpisah atau membungkusnya dengan kelas kosong:

class BaseTestCases:

    class BaseTest(unittest.TestCase):

        def testCommon(self):
            print('Calling BaseTest:testCommon')
            value = 5
            self.assertEqual(value, 5)


class SubTest1(BaseTestCases.BaseTest):

    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTestCases.BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

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

Hasil:

Calling BaseTest:testCommon
.Calling SubTest1:testSub1
.Calling BaseTest:testCommon
.Calling SubTest2:testSub2
.
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

6
Ini adalah favorit saya. Ini adalah cara paling tidak hacky dan tidak mengganggu metode utama, tidak mengubah MRO dan memungkinkan saya untuk mendefinisikan setUp, setUpClass dll di kelas dasar.
Hannes

6
Saya benar-benar tidak mengerti (dari mana sihir itu berasal?), Tapi ini jauh solusi terbaik menurut saya :) Datang dari Jawa, saya benci Multiple Inheritance ...
Edouard Berthe

4
@Edouardb unittest hanya menjalankan kelas tingkat modul yang mewarisi dari TestCase. Tetapi BaseTest bukan tingkat modul.
JoshB

Sebagai alternatif yang sangat mirip, Anda bisa mendefinisikan ABC di dalam fungsi no-args yang mengembalikan ABC ketika dipanggil
Anakhand

34

Anda dapat memecahkan masalah ini dengan satu perintah:

del(BaseTest)

Jadi kodenya akan terlihat seperti ini:

import unittest

class BaseTest(unittest.TestCase):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

del(BaseTest)

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

3
BaseTest adalah anggota modul saat sedang didefinisikan, sehingga tersedia untuk digunakan sebagai kelas dasar dari SubTests. Tepat sebelum definisi selesai, del () menghapusnya sebagai anggota, sehingga kerangka kerja yang paling tidak akan menemukannya ketika mencari subkelas TestCase dalam modul.
mhsmith

3
ini jawaban yang luar biasa! Saya suka lebih dari @MatthewMarshall karena dalam solusinya, Anda akan mendapatkan kesalahan sintaks dari pylint, karena self.assert*metode tidak ada dalam objek standar.
SimplyKnownAsG

1
Tidak berfungsi jika BaseTest direferensikan di tempat lain di kelas dasar atau subkelasnya, misalnya saat memanggil super () dalam metode penimpaan: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes Setidaknya dalam python 3, BaseTestdapat dirujuk melalui super(self.__class__, self)atau hanya super()dalam subkelas, meskipun tampaknya tidak jika Anda mewarisi konstruktor . Mungkin ada juga alternatif "anonim" ketika kelas dasar perlu referensi itu sendiri (bukan karena saya punya ide ketika kelas perlu referensi sendiri).
Stein

29

Jawaban Matthew Marshall bagus, tetapi mengharuskan Anda mewarisi dari dua kelas di setiap kasus uji Anda, yang rawan kesalahan. Sebagai gantinya, saya menggunakan ini (python> = 2.7):

class BaseTest(unittest.TestCase):

    @classmethod
    def setUpClass(cls):
        if cls is BaseTest:
            raise unittest.SkipTest("Skip BaseTest tests, it's a base class")
        super(BaseTest, cls).setUpClass()

3
Itu rapi. Apakah ada cara untuk menyiasati harus menggunakan loncatan? Bagi saya, lompatan tidak diinginkan dan digunakan untuk menunjukkan masalah dalam rencana pengujian saat ini (baik dengan kode atau tes)?
Zach Young

@ ZakaryYoung Saya tidak tahu, mungkin jawaban lain bisa membantu.
Dennis Golomazov

@ ZakaryYoung Saya sudah mencoba untuk memperbaiki masalah ini, lihat jawaban saya.
simonzack

itu tidak segera jelas apa yang secara inheren rawan tentang mewarisi dari dua kelas
jwg

@ jwg lihat komentar untuk jawaban yang diterima :) Anda harus mewarisi masing-masing kelas tes dari dua kelas dasar; Anda perlu mempertahankan urutannya yang benar; Jika Anda ingin menambahkan kelas tes dasar lainnya, Anda harus mewarisi darinya juga. Tidak ada yang salah dengan mixin, tetapi dalam hal ini mereka dapat diganti dengan lompatan sederhana.
Dennis Golomazov

7

Apa yang ingin Anda capai? Jika Anda memiliki kode tes umum (pernyataan, tes templat, dll), kemudian tempatkan dalam metode yang tidak diawali dengan testjadi unittesttidak akan memuatnya.

import unittest

class CommonTests(unittest.TestCase):
      def common_assertion(self, foo, bar, baz):
          # whatever common code
          self.assertEqual(foo(bar), baz)

class BaseTest(CommonTests):

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(CommonTests):

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)

class SubTest2(CommonTests):

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

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

1
Di bawah saran Anda, apakah common_assertion () masih dapat dijalankan secara otomatis saat menguji subclass?
Stewart

@ Mulailah Tidak, itu tidak akan. Pengaturan standar adalah hanya menjalankan metode yang dimulai dengan "test".
CS

6

Jawaban Matthew adalah yang perlu saya gunakan karena saya masih di 2,5. Tetapi mulai 2.7 Anda dapat menggunakan dekorator @ unittest.skip () pada metode pengujian apa pun yang ingin Anda lewati.

http://docs.python.org/library/unittest.html#skipping-tests-and-expected-failures

Anda harus menerapkan dekorator lompatan Anda sendiri untuk memeriksa jenis dasar. Belum pernah menggunakan fitur ini sebelumnya, tetapi dari atas kepala saya, Anda dapat menggunakan BaseTest sebagai jenis penanda untuk mengkondisikan lompatan:

def skipBaseTest(obj):
    if type(obj) is BaseTest:
        return unittest.skip("BaseTest tests skipped")
    return lambda func: func

6

Salah satu cara yang saya pikirkan untuk menyelesaikan ini adalah dengan menyembunyikan metode tes jika kelas dasar digunakan. Dengan cara ini tes tidak dilewati, sehingga hasil tes bisa menjadi hijau, bukan kuning di banyak alat pelaporan pengujian.

Dibandingkan dengan metode mixin, ide seperti PyCharm tidak akan mengeluh bahwa metode unit test hilang dari kelas dasar.

Jika kelas dasar mewarisi dari kelas ini, itu perlu mengganti metode setUpClassdan tearDownClass.

class BaseTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls._test_methods = []
        if cls is BaseTest:
            for name in dir(cls):
                if name.startswith('test') and callable(getattr(cls, name)):
                    cls._test_methods.append((name, getattr(cls, name)))
                    setattr(cls, name, lambda self: None)

    @classmethod
    def tearDownClass(cls):
        if cls is BaseTest:
            for name, method in cls._test_methods:
                setattr(cls, name, method)
            cls._test_methods = []

5

Anda bisa menambahkan __test_ = Falsedi kelas BaseTest, tetapi jika Anda menambahkannya, ketahuilah bahwa Anda harus menambahkan __test__ = Truedi kelas turunan untuk dapat menjalankan tes.

import unittest

class BaseTest(unittest.TestCase):
    __test__ = False

    def testCommon(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)

class SubTest1(BaseTest):
    __test__ = True

    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    __test__ = True

    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

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

Solusi ini tidak bekerja dengan test discovery / runner test unittest sendiri. (Saya percaya itu membutuhkan menggunakan pelari tes alternatif, seperti hidung.)
medmunds

4

Pilihan lain adalah tidak mengeksekusi

unittest.main()

Alih-alih itu bisa Anda gunakan

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

Jadi, Anda hanya menjalankan tes di kelas TestClass


Ini adalah solusi paling tidak hacky. Alih-alih mengubah apa yang unittest.main()dikumpulkan ke dalam suite default Anda membentuk suite eksplisit dan menjalankan tesnya.
zgoda

1

Saya membuat hal yang sama dari @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) tetapi sedikit dimodifikasi:

import unittest2


from some_module import func1, func2


def make_base_class(func):

    class Base(unittest2.TestCase):

        def test_common1(self):
            print("in test_common1")
            self.assertTrue(func())

        def test_common2(self):
            print("in test_common1")
            self.assertFalse(func(42))

    return Base



class A(make_base_class(func1)):
    pass


class B(make_base_class(func2)):

    def test_func2_with_no_arg_return_bar(self):
        self.assertEqual("bar", func2())

dan di sana kita pergi.


1

Pada Python 3.2, Anda bisa menambahkan fungsi test_loader ke modul untuk mengontrol tes mana (jika ada) yang ditemukan oleh mekanisme penemuan tes.

Misalnya, berikut ini hanya akan memuat poster asli SubTest1dan SubTest2Kasus Uji, mengabaikan Base:

def load_tests(loader, standard_tests, pattern):
    suite = TestSuite()
    suite.addTests([SubTest1, SubTest2])
    return suite

Ini seharusnya mungkin untuk iterate atas standard_tests(a TestSuiteyang berisi tes loader bawaan ditemukan) dan copy semua tapi Baseuntuk suitesebaliknya, tetapi sifat bersarang TestSuite.__iter__merek yang lebih banyak rumit.


0

Ubah nama metode testCommon menjadi sesuatu yang lain. Unittest (biasanya) melewatkan apa pun yang tidak memiliki 'tes' di dalamnya.

Cepat dan sederhana

  import unittest

  class BaseTest(unittest.TestCase):

   def methodCommon(self):
       print 'Calling BaseTest:testCommon'
       value = 5
       self.assertEquals(value, 5)

  class SubTest1(BaseTest):

      def testSub1(self):
          print 'Calling SubTest1:testSub1'
          sub = 3
          self.assertEquals(sub, 3)


  class SubTest2(BaseTest):

      def testSub2(self):
          print 'Calling SubTest2:testSub2'
          sub = 4
          self.assertEquals(sub, 4)

  if __name__ == '__main__':
      unittest.main()`

2
Ini akan menghasilkan tidak menjalankan tes methodCommon di salah satu dari SubTests.
Pepper Lebeck-Jobe

0

Jadi ini semacam utas lama tapi saya menemukan masalah ini hari ini dan memikirkan hack saya sendiri untuk itu. Ia menggunakan dekorator yang membuat nilai-nilai fungsi Tidak ada ketika diakses melalui kelas dasar. Tidak perlu khawatir tentang pengaturan dan pengaturan kelas karena jika baseclass tidak memiliki tes mereka tidak akan berjalan.

import types
import unittest


class FunctionValueOverride(object):
    def __init__(self, cls, default, override=None):
        self.cls = cls
        self.default = default
        self.override = override

    def __get__(self, obj, klass):
        if klass == self.cls:
            return self.override
        else:
            if obj:
                return types.MethodType(self.default, obj)
            else:
                return self.default


def fixture(cls):
    for t in vars(cls):
        if not callable(getattr(cls, t)) or t[:4] != "test":
            continue
        setattr(cls, t, FunctionValueOverride(cls, getattr(cls, t)))
    return cls


@fixture
class BaseTest(unittest.TestCase):
    def testCommon(self):
        print('Calling BaseTest:testCommon')
        value = 5
        self.assertEqual(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print('Calling SubTest1:testSub1')
        sub = 3
        self.assertEqual(sub, 3)


class SubTest2(BaseTest):

    def testSub2(self):
        print('Calling SubTest2:testSub2')
        sub = 4
        self.assertEqual(sub, 4)

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

0

Berikut adalah solusi yang hanya menggunakan fitur unittest yang terdokumentasi dan yang menghindari status "lewati" dalam hasil pengujian Anda:

class BaseTest(unittest.TestCase):

    def __init__(self, methodName='runTest'):
        if self.__class__ is BaseTest:
            # don't run these tests in the abstract base implementation
            methodName = 'runNoTestsInBaseClass'
        super().__init__(methodName)

    def runNoTestsInBaseClass(self):
        pass

    def testCommon(self):
        # everything else as in the original question

Cara kerjanya: sesuai unittest.TestCasedokumentasi , "Setiap instance TestCase akan menjalankan metode basis tunggal: metode ini bernama methodName." "RunTests" default menjalankan semua metode tes * di kelas — begitulah cara kerja TestCase biasanya. Tetapi ketika menjalankan dalam kelas dasar abstrak itu sendiri, Anda bisa menimpa perilaku itu dengan metode yang tidak melakukan apa-apa.

Efek sampingnya adalah jumlah tes Anda akan bertambah satu: "tes" runNoTestsInBaseClass akan dihitung sebagai tes yang berhasil ketika dijalankan pada BaseClass.

(Ini juga bekerja dengan Python 2.7, jika Anda masih pada itu. Hanya perubahan super()ke super(BaseTest, self).)


-2

Ubah nama metode BaseTest untuk mengatur:

class BaseTest(unittest.TestCase):
    def setUp(self):
        print 'Calling BaseTest:testCommon'
        value = 5
        self.assertEquals(value, 5)


class SubTest1(BaseTest):
    def testSub1(self):
        print 'Calling SubTest1:testSub1'
        sub = 3
        self.assertEquals(sub, 3)


class SubTest2(BaseTest):
    def testSub2(self):
        print 'Calling SubTest2:testSub2'
        sub = 4
        self.assertEquals(sub, 4)

Keluaran:

Berlari 2 tes dalam 0,000s

Calling BaseTest: testCommon Calling
SubTest1: testSub1 Calling
BaseTest: testCommon Calling
SubTest2: testSub2

Dari dokumentasi :

TestCase.setUp ()
Metode dipanggil untuk menyiapkan perlengkapan tes. Ini dipanggil segera sebelum memanggil metode pengujian; setiap pengecualian yang diajukan oleh metode ini akan dianggap sebagai kesalahan daripada kegagalan pengujian. Implementasi default tidak melakukan apa pun.


Itu akan berhasil, bagaimana jika saya memiliki n testCommon, haruskah saya menempatkan semuanya di bawah setUp?
Thierry Lam

1
Ya, Anda harus meletakkan semua kode yang bukan merupakan kasus uji aktual di bawah pengaturan.
Brian R. Bondy

Tetapi jika sebuah subclass memiliki lebih dari satu test...metode, setUpdieksekusi berulang-ulang, sekali per metode tersebut; jadi BUKAN itu ide yang bagus!
Alex Martelli

Tidak begitu yakin apa yang diinginkan OP dalam hal ketika dieksekusi dalam skenario yang lebih kompleks.
Brian R. Bondy
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.