Temel ve alt sınıf ile Python birim testi


153

Şu anda ortak bir test setini paylaşan birkaç birim testim var. İşte bir örnek:

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

Yukarıdakilerin çıktısı:

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

OK

İlkinin testCommonçağrılmaması için yukarıdakileri yeniden yazmanın bir yolu var mı ?

DÜZENLEME: Yukarıda 5 test çalıştırmak yerine, 2'si Alt Test1'den ve 2'si Alt Test2'den olmak üzere yalnızca 4 test çalıştırmasını istiyorum. Görünüşe göre Python unittest orijinal BaseTest'i kendi başına çalıştırıyor ve bunun olmasını önlemek için bir mekanizmaya ihtiyacım var.


Kimsenin bundan bahsetmediğini görüyorum, ancak ana bölümü değiştirme ve BaseTest'in tüm alt sınıflarını içeren bir test paketi çalıştırma seçeneğiniz var mı?
kon psych

Yanıtlar:


156

Çoklu kalıtım kullanın, böylece ortak testlere sahip sınıfınızın kendisi TestCase'den devralmaz.

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
Şimdiye kadarki en şık çözüm bu.
Thierry Lam

28
Bu yöntem, temel sınıfların sırasını tersine çevirirseniz yalnızca setUp ve tearDown yöntemleri için çalışır. Yöntemler unittest.TestCase içinde tanımlandığından ve super () 'i çağırmadıklarından, CommonTests'teki herhangi bir setUp ve tearDown yönteminin MRO'da ilk sırada olması gerekir, aksi takdirde bunlar çağrılmaz.
Ian Clelland

32
Sadece Ian Clelland'ın benim gibi insanlar için daha net olması için açıklamasını açıklığa kavuşturmak için: sınıfa yöntemler setUpve tearDownyöntemler eklerseniz ve CommonTeststüretilmiş sınıflardaki her test için bunların çağrılmasını istiyorsanız, temel sınıfların sırasını tersine çevirmeniz gerekir. böylece olacaktır: class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

6
Bu yaklaşımın gerçekten hayranı değilim. Bu sınıflar hem devralan gerektiğini kodunda bir sözleşme kurar unittest.TestCase ve CommonTests . Bence aşağıdaki setUpClassyöntem en iyisidir ve insan hatasına daha az meyillidir. Ya bu ya da BaseTest sınıfını, biraz daha zor olan ancak test çalıştırma çıktısında atlama mesajını önleyen bir konteyner sınıfına sarmak.
David Sanders

10
Bununla ilgili sorun, pylint'in uygun olmasıdır çünkü CommonTestso sınıfta var olmayan yöntemleri çağırmaktır.
MadScientist

148

Çoklu miras kullanmayın, sizi daha sonra ısırır .

Bunun yerine, temel sınıfınızı ayrı bir modüle taşıyabilir veya boş sınıfla sarmalayabilirsiniz:

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

Çıktı:

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

OK

6
Bu benim favorim. En az hacky yöntemidir ve geçersiz kılma yöntemlerine müdahale etmez, MRO'yu değiştirmez ve temel sınıfta setUp, setUpClass vb. Tanımlamama izin verir.
Hannes

6
Cidden anlamadım (sihir nereden geliyor?), Ama bana göre en iyi çözüm :) Java'dan geliyor, Çoklu Miras'tan nefret ediyorum ...
Edouard Berthe

4
@Edouardb unittest yalnızca TestCase'den devralan modül düzeyinde sınıfları çalıştırır. Ancak BaseTest, modül düzeyinde değildir.
JoshB

Çok benzer bir alternatif olarak, ABC'yi çağrıldığında ABC'yi döndüren bir no-args işlevi içinde tanımlayabilirsiniz
Anakhand

34

Bu sorunu tek bir komutla çözebilirsiniz:

del(BaseTest)

Yani kod şöyle görünecektir:

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, tanımlanırken modülün bir üyesidir, bu nedenle Alt Testlerin temel sınıfı olarak kullanılabilir. Tanım tamamlanmadan hemen önce del () onu bir üye olarak kaldırır, böylece unittest çerçevesi modülde TestCase alt sınıflarını aradığında onu bulamaz.
mhsmith

3
bu harika bir cevap! Bunu @ MatthewMarshall'dan daha çok seviyorum çünkü onun çözümünde, pylint'ten sözdizimi hataları alacaksınız çünkü self.assert*yöntemler standart bir nesnede mevcut değil.
SimplyKnownAsG

1
BaseTest'e temel sınıfta veya alt sınıflarında başka herhangi bir yerde başvurulursa çalışmaz, örneğin yöntem geçersiz kılmalarında super () çağrılırken: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes En azından python 3'te alt sınıflar BaseTestaracılığıyla super(self.__class__, self)veya sadece super()alt sınıflarda referans verilebilir , ancak görünüşe göre yapıcıları miras alacaksanız değil . Belki de temel sınıfın kendisine referans vermesi gerektiğinde böyle bir "anonim" alternatif vardır (bir sınıfın kendisine referans vermesi gerektiğinde hiçbir fikrim olmadığından değil).
Stein

29

Matthew Marshall'ın cevabı harika, ancak test durumlarınızın her birinde iki sınıftan miras almanızı gerektiriyor ki bu hataya açık. Bunun yerine, bunu kullanıyorum (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
Bu harika. Atlama kullanmak zorunda kalmanın bir yolu var mı? Bana göre, atlamalar istenmeyen bir durumdur ve mevcut test planındaki bir sorunu belirtmek için kullanılır (kodla veya testle birlikte)?
Zach Young

@ZacharyYoung Bilmiyorum, belki başka cevaplar yardımcı olabilir.
Dennis Golomazov

@ZacharyYoung Bu sorunu çözmeye çalıştım, cevabıma bakın.
simonzack

iki sınıftan miras
almanın

@jwg kabul edilen cevaba yapılan yorumları görün :) Test sınıflarınızın her birini iki temel sınıftan devralmanız gerekir; bunların doğru sırasını korumanız gerekir; başka bir temel test sınıfı eklemek isterseniz, ondan da miras almanız gerekir. Mixinlerde yanlış bir şey yoktur, ancak bu durumda basit bir atlama ile değiştirilebilirler.
Dennis Golomazov

8

__test__ = FalseBaseTest sınıfına ekleyebilirsiniz , ancak bunu eklerseniz, __test__ = Truetestleri çalıştırabilmek için türetilmiş sınıflar eklemeniz gerektiğini unutmayın .

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

Bu çözüm, unittest'in kendi test keşfi / test çalıştırıcısı ile çalışmaz. (Burun gibi alternatif bir test koşucusu kullanmayı gerektirdiğine inanıyorum.)
medmunds

7

Ne elde etmeye çalışıyorsun Ortak test kodunuz varsa (iddialar, şablon testleri, vb.), Onları önekli olmayan yöntemlere yerleştirin, testbu yüzden unittestonları yüklemeyin.

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
Önerinize göre, alt sınıfları test ederken common_assertion () yine de otomatik olarak çalıştırılacak mı?
Stewart

@Stewart Hayır olmaz. Varsayılan ayar, yalnızca "test" ile başlayan yöntemleri çalıştırmaktır.
CS

6

Matthew'un cevabı hala 2.5'te olduğum için kullanmam gereken cevap. Ancak 2.7'den itibaren @ unittest.skip () dekoratörünü atlamak istediğiniz herhangi bir test yönteminde kullanabilirsiniz.

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

Temel türü kontrol etmek için kendi atlama dekoratörünüzü uygulamanız gerekecek. Bu özelliği daha önce kullanmamıştım, ancak aklımın ucunda BaseTest'i atlamayı koşullandırmak için bir işaretçi türü olarak kullanabilirsiniz :

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

6

Bunu çözmeyi düşündüğüm bir yol, temel sınıf kullanılıyorsa test yöntemlerini gizlemektir. Bu şekilde testler atlanmaz, bu nedenle test sonuçları birçok test raporlama aracında sarı yerine yeşil olabilir.

Mixin yöntemiyle karşılaştırıldığında, PyCharm gibi ide'ler, birim test yöntemlerinin temel sınıftan eksik olduğundan şikayet etmez.

Bir temel sınıf bu sınıftan miras alırsa, setUpClassve tearDownClassyöntemlerini geçersiz kılması gerekir .

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

Başka bir seçenek yürütmemek

unittest.main()

Bunun yerine kullanabilirsin

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

Yani sadece sınıftaki testleri yaparsınız TestClass


Bu en az hackli çözümdür. unittest.main()Varsayılan pakette toplananları değiştirmek yerine, açık bir paket oluşturur ve testlerini çalıştırırsınız.
zgoda

1

@Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) ile aynı şeyi yaptım ama biraz değiştirdim:

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

ve oraya gidiyoruz.


1

Python 3.2'den itibaren bir test_loader ekleyebilirsiniz test keşif mekanizması tarafından hangi testlerin (varsa) bulunduğunu kontrol etmek için bir modüle işlevi .

Örneğin, aşağıdakiler göz ardı edilerek yalnızca orijinal posterleri SubTest1ve SubTest2Test Durumlarını yükleyecektir Base:

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

Bu yineleme mümkün olmalı standard_tests(bir TestSuitevarsayılan yükleyici bulundu testleri içerir) ve tüm ama kopya Baseetmek suiteyerine, ama iç içe geçmiş doğasını TestSuite.__iter__çok daha karmaşık olduğunu yapar.


0

Sadece testCommon yöntemini başka bir adla yeniden adlandırın. Unittest (genellikle) içinde 'test' olmayan her şeyi atlar.

Hızlı ve basit

  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
Bu, herhangi bir Alt Testte methodCommon testinin çalıştırılmamasından kaynaklanır.
Pepper Lebeck-Jobe

0

Yani bu biraz eski bir konu ama bugün bu problemle karşılaştım ve bunun için kendi hack'imi düşündüm. Temel sınıf üzerinden erişildiğinde işlevlerin değerlerini None yapan bir dekoratör kullanır. Kurulum ve kurulum sınıfı hakkında endişelenmenize gerek yok çünkü temel sınıfta test yoksa çalışmazlar.

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

Burada, yalnızca belgelenmiş birim testi özelliklerini kullanan ve test sonuçlarınızda "atlama" durumuna sahip olmanızı önleyen bir çözüm bulunmaktadır:

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

Nasıl çalışır: unittest.TestCaseBelgelere göre , "Her TestCase örneği tek bir temel yöntemi çalıştırır: methodName adlı yöntem." Varsayılan "runTests", sınıftaki tüm test * yöntemlerini çalıştırır; TestCase örnekleri normalde bu şekilde çalışır. Ancak soyut temel sınıfta çalışırken, hiçbir şey yapmayan bir yöntemle bu davranışı basitçe geçersiz kılabilirsiniz.

Bir yan etki, test sayınızın bir artmasıdır: runNoTestsInBaseClass "testi", BaseClass üzerinde çalıştırıldığında başarılı bir test olarak sayılır.

(Bu aynı zamanda Python 2.7'de de çalışır, eğer hala devam ediyorsanız. super() için super(BaseTest, self).)


-2

BaseTest yöntem adını ayarlamak için değiştirin:

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)

Çıktı:

0.000 saniyede 2 test yapıldı

BaseTest'i çağırma
: testCommon
SubTest1'i çağırıyor: testSub1
BaseTest'i çağırıyor: testCommon SubTest2'yi çağırıyor: testSub2

Gönderen belgeler :

TestCase.setUp ()
Yöntem, test fikstürünü hazırlamak için çağrıldı. Bu, test yöntemi çağrılmadan hemen önce çağrılır; bu yöntemle ortaya çıkan herhangi bir istisna, test başarısızlığından çok hata olarak kabul edilecektir. Varsayılan uygulama hiçbir şey yapmaz.


Bu işe yarar, ya n testCommon'a sahipsem hepsini altına setUpmı yerleştirmeliyim ?
Thierry Lam

1
Evet, gerçek bir test durumu olmayan tüm kodları kurulumun altına koymalısınız.
Brian R. Bondy

Ancak bir alt sınıfın birden fazla test...yöntemi varsa, setUpbu yöntem başına bir kez olmak üzere defalarca çalıştırılır; bu yüzden oraya testler koymak iyi bir fikir DEĞİL!
Alex Martelli

Daha karmaşık bir senaryoda yürütüldüğünde OP'nin ne istediğinden tam olarak emin değilim.
Brian R. Bondy
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.