Kiểm tra đơn vị Python với lớp cơ sở và lớp con


148

Tôi hiện có một vài bài kiểm tra đơn vị chia sẻ một bộ bài kiểm tra chung. Đây là một ví dụ:

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

Đầu ra của ở trên là:

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

OK

Có cách nào để viết lại ở trên để cái đầu tiên testCommonkhông được gọi không?

EDIT: Thay vì chạy 5 thử nghiệm ở trên, tôi muốn nó chỉ chạy 4 thử nghiệm, 2 từ SubTest1 và 2 thử nghiệm khác từ SubTest2. Có vẻ như Python unittest đang tự chạy BaseTest ban đầu và tôi cần một cơ chế để ngăn điều đó xảy ra.


Tôi thấy không có ai đề cập đến nó nhưng bạn có tùy chọn thay đổi phần chính và chạy một bộ thử nghiệm có tất cả các lớp con của BaseTest không?
kon tâm lý

Câu trả lời:


154

Sử dụng nhiều kế thừa, do đó, lớp của bạn với các bài kiểm tra phổ biến không tự thừa hưởng từ 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
Đó là giải pháp thanh lịch nhất cho đến nay.
Thierry Lam

27
Phương thức này chỉ hoạt động đối với các phương thức setUp và tornDown nếu bạn đảo ngược thứ tự của các lớp cơ sở. Bởi vì các phương thức được định nghĩa trong unittest.TestCase và chúng không gọi super (), nên bất kỳ phương thức setUp và tornDown nào trong CommonTests cần phải là đầu tiên trong MRO, hoặc chúng sẽ không được gọi.
Ian Clelland

32
Chỉ cần làm rõ nhận xét của Ian Clelland để mọi người như tôi rõ ràng hơn: nếu bạn thêm setUptearDownphương thức vào CommonTestslớp, và bạn muốn chúng được gọi cho mỗi bài kiểm tra trong các lớp dẫn xuất, bạn phải đảo ngược thứ tự của các lớp cơ sở, vì vậy nó sẽ là : class SubTest1(CommonTests, unittest.TestCase).
Dennis Golomazov

6
Tôi không thực sự là một fan hâm mộ của phương pháp này. Điều này thiết lập một hợp đồng trong mã mà các lớp phải kế thừa từ cả hai unittest.TestCase CommonTests . Tôi nghĩ rằng setUpClassphương pháp dưới đây là tốt nhất và ít bị lỗi của con người. Hoặc là hoặc bọc lớp BaseTest trong một lớp container có phần hack hơn một chút nhưng tránh thông báo bỏ qua trong bản in chạy thử.
David Sanders

10
Vấn đề với cái này là pylint có phù hợp bởi vì CommonTestsđang gọi các phương thức không tồn tại trong lớp đó.
MadSellectist

145

Không sử dụng nhiều thừa kế, nó sẽ cắn bạn sau này .

Thay vào đó, bạn chỉ có thể di chuyển lớp cơ sở của mình vào mô-đun riêng hoặc bọc nó bằng lớp trống:

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

Đầu ra:

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

OK

6
Đây là yêu thích của tôi. Đó là phương tiện ít hack nhất và không can thiệp vào các phương thức ghi đè, không làm thay đổi MRO và cho phép tôi xác định setUp, setUpClass, v.v. trong lớp cơ sở.
Hannes

6
Tôi thực sự không hiểu nó (ma thuật đến từ đâu?), Nhưng đó là giải pháp tốt nhất theo tôi :) Đến từ Java, tôi ghét nhiều quyền thừa kế ...
Edouard Berthe

4
@Edouardb unittest chỉ chạy các lớp cấp mô-đun kế thừa từ TestCase. Nhưng BaseTest không phải là cấp độ mô-đun.
JoshB

Là một thay thế rất giống nhau, bạn có thể định nghĩa ABC bên trong hàm no-args trả về ABC khi được gọi
Anakhand

34

Bạn có thể giải quyết vấn đề này bằng một lệnh duy nhất:

del(BaseTest)

Vì vậy, mã sẽ trông như thế này:

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 là một thành viên của mô-đun trong khi nó đang được xác định, vì vậy nó có sẵn để sử dụng làm lớp cơ sở của SubTests. Ngay trước khi định nghĩa hoàn tất, del () sẽ loại bỏ nó như một thành viên, vì vậy khung công tác nhỏ nhất sẽ không tìm thấy nó khi nó tìm kiếm các lớp con TestCase trong mô-đun.
mhsmith

3
đây là một câu trả lời tuyệt vời Tôi thích nó hơn @MatthewMarshall vì trong giải pháp của anh ấy, bạn sẽ gặp lỗi cú pháp từ pylint, vì các self.assert*phương thức không tồn tại trong một đối tượng tiêu chuẩn.
SimplyKnownAsG

1
Không hoạt động nếu BaseTest được tham chiếu ở bất kỳ nơi nào khác trong lớp cơ sở hoặc các lớp con của nó, ví dụ như khi gọi super () trong phương thức ghi đè: super( BaseTest, cls ).setUpClass( )
Hannes

1
@Hannes Ít nhất trong python 3, BaseTestcó thể được tham chiếu qua super(self.__class__, self)hoặc chỉ super()trong các lớp con, mặc dù rõ ràng là không nếu bạn kế thừa các hàm tạo . Có lẽ cũng có một sự thay thế "ẩn danh" như vậy khi lớp cơ sở cần tham chiếu chính nó (không phải là tôi có ý tưởng nào khi một lớp cần tham chiếu chính nó).
Stein

28

Câu trả lời của Matthew Marshall rất hay, nhưng nó đòi hỏi bạn phải kế thừa từ hai lớp trong mỗi trường hợp kiểm tra, dễ bị lỗi. Thay vào đó, tôi sử dụng cái này (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
Thật gọn gàng. Có cách nào để có được xung quanh phải sử dụng bỏ qua? Đối với tôi, bỏ qua là không thể chấp nhận được và được sử dụng để chỉ ra một vấn đề trong kế hoạch kiểm tra hiện tại (với mã hoặc thử nghiệm)?
Zach Young

@ZacharyYoung Tôi không biết, có lẽ những câu trả lời khác có thể giúp ích.
Dennis Golomazov

@ZacharyYoung Tôi đã cố gắng khắc phục vấn đề này, xem câu trả lời của tôi.
simonzack

không rõ ràng ngay lập tức những gì dễ bị lỗi khi kế thừa từ hai lớp
jwg

@jwg xem bình luận cho câu trả lời được chấp nhận :) Bạn cần kế thừa từng lớp kiểm tra của mình từ hai lớp cơ sở; bạn cần giữ đúng thứ tự của chúng; nếu bạn muốn thêm một lớp kiểm tra cơ sở khác, bạn cũng cần phải kế thừa từ nó. Không có gì sai với mixins, nhưng trong trường hợp này chúng có thể được thay thế bằng một cách bỏ qua đơn giản.
Dennis Golomazov

7

Bạn đang cố gắng để đạt được điều gì? Nếu bạn có mã kiểm tra chung (xác nhận, kiểm tra mẫu, v.v.), thì hãy đặt chúng vào các phương thức không có tiền tố testđể unittestkhông tải chúng.

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
Theo đề xuất của bạn, common_asserts () có còn được chạy tự động khi kiểm tra các lớp con không?
Stewart

@Stewart Không, nó sẽ không. Cài đặt mặc định là chỉ chạy các phương thức bắt đầu bằng "test".
CS

6

Câu trả lời của Matthew là câu tôi cần sử dụng vì tôi vẫn còn trên 2.5. Nhưng kể từ 2.7, bạn có thể sử dụng trình trang trí @ unittest.skip () trên bất kỳ phương pháp thử nghiệm nào bạn muốn bỏ qua.

http://docs.python.org/l Library / unittest.html # skipping-tests-and-

Bạn sẽ cần phải thực hiện trang trí bỏ qua của riêng bạn để kiểm tra loại cơ sở. Trước đây tôi chưa sử dụng tính năng này, nhưng ngoài đỉnh đầu tôi có thể sử dụng BaseTest làm loại đánh dấu để điều kiện bỏ qua:

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

5

Một cách tôi đã nghĩ để giải quyết điều này là bằng cách ẩn các phương thức kiểm tra nếu lớp cơ sở được sử dụng. Bằng cách này, các bài kiểm tra không bị bỏ qua, vì vậy kết quả kiểm tra có thể có màu xanh thay vì màu vàng trong nhiều công cụ báo cáo kiểm tra.

So với phương thức mixin, các ide như PyCharm sẽ không phàn nàn rằng các phương thức kiểm tra đơn vị bị thiếu trong lớp cơ sở.

Nếu một lớp cơ sở kế thừa từ lớp này, nó sẽ cần ghi đè lên các phương thức setUpClasstearDownClass.

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

Bạn có thể thêm __test_ = Falsevào lớp BaseTest, nhưng nếu bạn thêm nó, hãy lưu ý rằng bạn phải thêm __test__ = Truevào các lớp dẫn xuất để có thể chạy thử nghiệm.

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

4

Một lựa chọn khác là không thực thi

unittest.main()

Thay vào đó bạn có thể sử dụng

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

Vì vậy, bạn chỉ thực hiện các bài kiểm tra trong lớp TestClass


Đây là giải pháp ít hack nhất. Thay vì sửa đổi những gì unittest.main()thu thập vào bộ mặc định, bạn tạo thành bộ rõ ràng và chạy thử nghiệm.
zgoda

1

Tôi đã thực hiện tương tự như @Vladim P. ( https://stackoverflow.com/a/25695512/2451329 ) nhưng đã sửa đổi một chút:

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

và chúng tôi đi


1

Kể từ Python 3.2, bạn có thể thêm hàm test_loader vào mô-đun để kiểm soát các thử nghiệm (nếu có) được tìm thấy bởi cơ chế khám phá thử nghiệm.

Ví dụ: những điều sau đây sẽ chỉ tải các bài đăng SubTest1SubTest2Trường hợp kiểm tra ban đầu , bỏ qua Base:

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

Nó nên có thể để lặp qua standard_tests(một TestSuitechứa các bài kiểm tra bộ nạp mặc định tìm thấy) và sao chép tất cả nhưng Baseđể suitethay vào đó, nhưng bản chất lồng nhau của TestSuite.__iter__làm cho rằng rất nhiều phức tạp hơn.


0

Chỉ cần đổi tên phương thức testCommon thành một cái gì đó khác. Unittest (thường) bỏ qua bất cứ thứ gì không có 'kiểm tra' trong đó.

Nhanh chóng và đơn giản

  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
Điều này sẽ có kết quả của việc không chạy thử nghiệm phương thứcCommon trong một trong các SubTests.
Pepper Lebeck-Jobe

0

Vì vậy, đây là một loại chủ đề cũ nhưng tôi đã gặp vấn đề này ngày hôm nay và nghĩ về việc hack của riêng tôi cho nó. Nó sử dụng một trình trang trí làm cho các giá trị của các hàm Không có khi được truy cập thông qua lớp cơ sở. Đừng lo lắng về việc thiết lập và thiết lập lớp vì nếu lớp cơ sở không có kiểm tra thì chúng sẽ không chạy.

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

-2

Thay đổi tên phương thức BaseTest thành setUp:

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)

Đầu ra:

Chạy thử nghiệm 2 trong 0.000 giây

Gọi BaseTest: testCommon Gọi
SubTest1: testSub1 Gọi
BaseTest: testCommon Gọi
SubTest2: testSub2

Từ tài liệu :


Phương thức TestCase.setUp () được gọi để chuẩn bị đồ gá thử nghiệm. Điều này được gọi ngay trước khi gọi phương thức thử nghiệm; bất kỳ ngoại lệ nào được nêu ra bằng phương pháp này sẽ được coi là một lỗi thay vì lỗi kiểm tra. Việc thực hiện mặc định không có gì.


Điều đó sẽ làm việc, nếu tôi có n testCommon, tôi có nên đặt tất cả chúng ở dưới setUpkhông?
Thierry Lam

1
Có, bạn nên đặt tất cả mã không phải là trường hợp thử nghiệm thực tế trong setUp.
Brian R. Bondy

Nhưng nếu một lớp con có nhiều hơn một test...phương thức, setUpđược thực hiện lặp đi lặp lại, một lần cho mỗi phương thức đó; Vì vậy, KHÔNG nên để thử nghiệm ở đó!
Alex Martelli

Không thực sự chắc chắn những gì OP muốn về mặt khi được thực hiện trong một kịch bản phức tạp hơn.
Brian R. Bondy
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.