Мне интересно, какие методы люди используют для упрощения «размера» кода, используемого для модульного тестирования. Например, я пытался маршалировать объект класса и тестировать маршалированный объект (но это предполагает, что маршалинг работает правильно).

Рассмотрим класс

import unittest
class Nums(object):
    def __init__(self, n1_, n2_, n3_):
        self.n1, self.n2, self.n3 = n1_, n2_, n3_
def marshal(self):
    return "n1 %g, n2 %g, n3 %g"%(self.n1,self.n2,self.n3)

А затем тесты на основе маршалинга, списки и обычные тесты

class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nu = Nums(10,20,30)
    def test_init1(self):
        self.assertEquals(self.nu.marshal(),"n1 %g, n2 %g, n3 %g"%(10,20,30))
    def test_init2(self):
        self.assertEquals([self.nu.n1,self.nu.n2,self.nu.n3],[10,21,31])
    def test_init3(self):
        self.assertEquals(self.nu.n1,10)
        self.assertEquals(self.nu.n2,21)
        self.assertEquals(self.nu.n3,31)

Которые дают следующие ошибки (поскольку, 20! = 21 и 30! = 31, мой тест имеет плохую инициализацию или условия теста неверны)

AssertionError: 'n1 10, n2 20, n3 30' != 'n1 10, n2 21, n3 31'

AssertionError: [10, 20, 30] != [10, 21, 31]

AssertionError: 20 != 21

Первое и второе сообщения об ошибках трудно понять (поскольку вам нужно проанализировать строку или список). Тем не менее, третий метод быстро увеличивает количество кода, используемого для тестирования сложных объектов.

Есть ли лучший способ упростить модульные тесты без создания сложных сообщений об ошибках? А без зависимости от правдивости маршала?

[изменено test_marshal на marshal]

5
Katya B 22 Июн 2010 в 18:46

2 ответа

Лучший ответ

Для инициализации тестирования я рекомендую не тестировать через вызов функции marshal(). Затем вам нужно не только определить, какой инициализатор вышел из строя, вам также нужно выяснить, не работает ли это ваша инициализация или сама функция маршалинга. «Минимальный стиль» для модульных тестов, который я бы порекомендовал, - это сузить фокус того, что вы тестируете, в любой точке тестирования, насколько это возможно.

Если бы мне действительно нужно было протестировать инициализацию целой группы полей, я мог бы провести рефакторинг почти так же, как Eli:

class MyTestCase(unittest.TestCase):
    def assertFieldsEqual(self, obj, expectedFieldValDict):
        for fieldName, fieldVal in expectedFieldValDict.items():
            self.assertFieldEqual(obj, fieldName, fieldVal)
    def assertFieldEqual(self, obj, fieldName, expectedFieldVal):
        actualFieldVal = getattr(obj, fieldName)
        if expectedFieldVal != actualFieldVal:
            msg = "Field %r doesn't match: expected %r, actual %r" % \
                (fieldName, expectedFieldVal, actualFieldVal)
            self.fail(msg)

class NumsTests(MyTestCase):
    def setUp(self):
        self.initFields = {'n1': 10, 'n2': 20, 'n3': 30}
        self.nums = Nums(**initFields)
    def test_init(self):
        self.assertFieldsEqual(self.nums, self.initFields)

«Добрый день, - слышу вы, - это много кода!» Ну да, но отличия заключаются в следующем:

  • assertFieldsEqual и assertFieldEqual - это функции многократного использования, которые были абстрагированы до общего класса тестового примера, который могут повторно использовать другие ваши тестовые примеры.
  • Сообщение об ошибке точно описывает, что происходит.

Когда придет время проверить вашу маршал-функцию, вы можете просто сделать это:

def test_marshal(self):
    expected = '...'
    self.assertEqual(expected, self.nums.marshal())

Однако при сравнении строк я предпочитаю метод, который сообщает мне, где именно строки расходятся. Для многострочных строк теперь есть метод для этого в Python 2.7, но в прошлом я использовал собственные методы для этой цели.

2
Owen S. 22 Июн 2010 в 17:10

Я повторяю приведенные выше комментарии о том, что у вас не должно быть тестовых методов для реального класса, который вы тестируете. Такие функции, как test_marshal, следует размещать в другом месте (при условии, что они существуют для тестирования, а не для общего использования), обычно в ваших файлах модульного теста. Однако, отложив это на время, я бы сделал что-то вроде этого

import unittest

class Nums(object):
    FORMAT = "n1 %g, n2 %g, n3 %g"  # make this a variable for easy testing

    def __init__(self, n1, n2, n3):
        self.n1, self.n2, self.n3 = n1, n2, n3  # no underscores necessary

    def test_marshal(self):
        return self.FORMAT % (self.n1, self.n2, self.n3)


class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nums = [10, 20, 30]    # make a param list variable to avoid duplication
        self.nu = Nums(*self.nums)  # Python "apply" syntax
        self.nu_nums = [self.nu.n1, self.nu.n2, self.nu.n3]  # we'll use this repeatedly

    def test_init1(self):
        self.assertEquals(self.nu.test_marshal(), self.nu.FORMAT % self.nums )

    def test_init2(self):
        self.assertEquals(self.nums, self.nu_nums)

    def test_init3(self):
        for reference, test in zip(self.nums, self.nu_nums):
            self.assertEquals(reference, test)

См. http://docs.python.org/library/functions.html#apply для объяснения использованного выше синтаксиса apply.

Помещая то, что вы тестируете, в переменные, вы можете избежать дублирования кода, что, по-видимому, является вашей основной задачей.

Что касается сбивающих с толку сообщений об ошибках, я думаю, это зависит от того, сколько подробностей вам нужно. Лично тот факт, что мои модульные тесты дают мне строку кода и значения, которые ожидались и отсутствовали, как правило, довольно ясно показывает, что пошло не так. Однако, если вам ДЕЙСТВИТЕЛЬНО нужно что-то, в котором конкретно указано, какое поле неверно, а не только значения, которые не совпадают, И вы хотите избежать дублирования кода, вы можете написать что-то вроде этого:

class NumsTests(unittest.TestCase):
    def setUp(self):
        self.nums = {"n1": 10, "n2": 20, "n3": 30}  # use a dict, not a list
        self.nu = Nums(**self.nums)                 # same Python "apply" syntax

    # test_init1 and test_init2 omitted for space

    def test_init3(self):
        for attr,val in self.nums.items():
            self.assertEqual([attr, val], [attr, getattr(self.nu, val)])

Если у вас когда-либо были несовпадающие значения, теперь вы получите ошибки, которые выглядят как

AssertionError: ["n1", 10] != ["n1", 11]

И, таким образом, вы сразу узнаете, какое поле не соответствует, вместо того, чтобы рассуждать об этом на основе того, какие значения были. Этот подход по-прежнему сохраняет проблему расширения кода, поскольку test_init3 останется того же размера независимо от того, сколько параметров вы добавите в свой класс Nums.

Обратите внимание, что для этого использования getattr необходимо, чтобы параметры __init__ то же имя, что и поля в классе num, например они должны называться n1, а не n1_ и т. д. Альтернативным подходом было бы использование __ dict__, как описано здесь.

3
Eli Courtwright 22 Июн 2010 в 15:23