Автоматизирано (unit) тестване

„ Програмиране с Python“, ФМИ

12.04.2011

Disclaimer

Днес няма да си говорим за acceptance testing, quality assurance или нещо, което се прави от „по-низшия“ отдел във фирмата. Всичко тук е дело на програмиста.

Митът

Проектът идва с готово, подробно задание. Прави се дизайн. С него работата се разбива на малки задачи. Те се извършват последователно. За всяка от тях пишете кода и приключвате. Изискванията не се променят, нито се добавя нова функционалност.

Митът v1.1

Щом съм написал един код, значи ми остава единствено да го разцъкам - няколко print-а, малко пробване в main метода/функцията и толкова. Така или иначе няма да се променя. А ако (не дай си боже) това се случи - аз съм го писал, знам го, няма как да допусна грешка. Най-много да го поразцъкам още малко.

Тежката действителност

Традиционния подход

class Programmer(object):
    # ...
    def implement_a_change(self, project, change):
        files = self.open_related_files(project, change)
        while True:
            self.attempt_change(change, files)
            project.run()
            result = self.click_around_and_test(project)
            project.stop()
            if result.successful(): break
        self.commit_code(project, files)
        self.hope_everything_went_ok()

Идея

— Добре де… хващам се, че постоянно правя едно и също нещо като робот. Понеже е досадно, лесно ще забравя нещо. Пък и само ми губи времето. Човешката цивилизация не реши ли тоя вид проблеми с някакви машини? Май се казваха компютри?

 

— Защо просто не си напишеш програма, която да го прави вместо теб?

На хартия (или проектор)

Кодът, който ще тестваме

class Interval(object):
    def __init__(self, left, right): self.left, self.right = left, right
    def __repr__(self): return "Interval({0}, {1})".format(self.left, self.right)
    def __eq__(self, other):
        return isinstance(other, Interval) and \
            (self.left, self.right) == (other.left, other.right)

    def left_open(self): return self.left == None
    def right_open(self): return self.right == None

    def contains_number(self, number):
        if self.left_open() and self.right_open(): return True
        if self.left_open(): return number <= self.right
        if self.right_open(): return self.left <= number
        return self.left < number < self.right

    def intersect(self, other):
        extr = lambda a, b, func: func(a, b) if not None in (a, b) else a or b
        return Interval(
            extr(self.left, other.left, max),
            extr(self.right, other.right, min))

    __and__ = intersect

Идеята…

class IntervalTest:
    def test_contains_number(self):
        interval = Interval(None, 0)
        твърдя_че("interval съдържа -3")
        твърдя_че("interval съдържа 0")
        твърдя_че("interval не съдържа 9")
        твърдя_че("interval.left_open() е истина")
        твърдя_че("interval.right_open() е лъжа")

    def test_intersects(self):
        твърдя_че("сечението на [0, 10] с [5, None] е [5, 10]")
        твърдя_че("сечението на [None, 0] с [None, 42] е [None, 0]")
        твърдя_че("сечението на [None, 20] с [-20, None] е [-20, 20]")
        твърдя_че("сечението на [None, 0] с [-10, None] е [-10, 0]")

…реализацията…

class IntervalTest(unittest.TestCase):
    def test_contains_number(self):
        interval = Interval(None, 0)
        self.assertTrue(interval.contains_number(-3))
        self.assertTrue(interval.contains_number(0))
        self.failIf(interval.contains_number(9))
        self.assertTrue(interval.left_open())
        self.failIf(interval.right_open())

    def test_intersects(self):
        self.assertEquals(
            Interval(5, 10), Interval(0, 10) & Interval(5, None))
        self.assertEquals(
            Interval(None, 0), Interval(None, 42) & Interval(None, 0))
        self.assertEquals(
            Interval(-20, 20), Interval(None, 20) & Interval(-20, None))
        self.assertEquals(
            Interval(-10, 0), Interval(None, 0) & Interval(-10, None))

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

…и резултата

.F
======================================================================
FAIL: test_intersects (__main__.IntervalTest)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "", line 52, in test_intersects
AssertionError: Interval(-10, 0) != Interval(-10, None)

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

bulgarian: (english, python)

vocabulary = {
    "група": ("test case", unittest.TestCase),
    "сценарий": ("test method", 
        [_ for _ in dir(YourTestCase) if _.startswith("test")]), 
    "твърдение": ("assertion",
        [_ for _ in dir(unittest.TestCase) if re.match("assert|fail", _)])
}
Важно. Не бъркайте ключовата дума assert с методите за твърдения в тестовете. Първото служи да прекратите програмата ако изпадне в невалидно състояние. Второто е част от библиотеката за тестове.

Твърдения в unittest.TestCase

Всички методи имат опционален последен аргумент msg - текстово съобщение, което ще се покаже ако теста пропадне.

Декоратори

class SomeText(TestCase):
    @skip("This is not yet implemented")
    def test_something(self):
        ...

Декоратори (2)

Видове тестове

За какво ни помагат тестовете

За какво не служат тестовете

Още речник

Документация

class Foo:
    """
    Sample Foo class
    """

    def foo(self):
        """
        Sample foo method
        Returns: 2
        """
        return 2

Документацията като тестове

def add(a, b):
    """
    Adds the two arguments.

    >>> add(1, 3)
    4
    >>> add(1, '')
    Traceback (most recent call last):
        ...
    TypeError: unsupported operand type(s) for +: 'int' and 'str'
    """
    return a + b

if __name__ == '__main__':
    import doctest
    doctest.testmod()

Дизайн

Въпрос: какво е „дизайн“ на едно приложение?

Test-Driven Development

Test-Driven Development is not about testing.

— Dan North

Test-Driven Development (2)

  1. Добави тест
  2. Пусни всички тестове и виж, че новия се чупи
  3. Напиши код
  4. Пусни тестовете и виж че минават успешно
  5. Подобри кода (refactor)
  6. Повтаряй

Демо

лишън!

Test-Driven Development (2)

Behaviour-Driven Development

Шепа съвети

Още въпроси?