Програмиране с Python: Автоматизирано (unit) тестване
Author: Стефан Кънев, Николай Бачийски и Димитър Димитров

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

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

#### self.assertEquals(set(lecturers),
set(['Стефан Кънев', 'Николай Бачийски', 'Димитър Димитров']))

#### 21.04.2008

# Disclaimer

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

# Митът

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

# Митът v1.1

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

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

- Заданията винаги се променят.
- Често се налага един код да се преработва.
- Писането на код е сложна задача - допускат се грешки.
- Програмистите (освен Мило) са хора - допускат грешки (освен Мило).
- Промяната на модул в единия край на системата като нищо може да счупи модул в другия край на системата.

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

    class Programmer(object):
        # ...
        def implementAChange(self, project, change):
            files = self.openRelatedFiles(project, change)
            while True:
                self.attemptChange(change, files)
                project.run()
                result = self.clickAroundAndTest(project)
                project.stop()
                if result.successful(): break
            self.commitCode(project, files)
            self.hopeEverythingWentOK()

# Идея

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

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

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

- За всичко съмнително ще пишем сценарий, който да „цъка“.
- Всеки сценарий ще изпълнява кода и ще прави няколко твърдения за резултатите.
- Сценариите ще бъдат обединени в групи.
- Пускате всички тестове с едно бутонче.
- Резултатът е „Всичко мина успешно“ или „Твърдения X, Y и Z в сценарии A, B и C се оказаха неверни“.

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

    class Interval(object):

        def __init__(self, left, right): self.left, self.right = left, right
        def __repr__(self): return "Interval(%s, %s)" % (self.left, self.right)
        def __eq__(self, other):
            return isinstance(other, Interval) and \
                (self.left, self.right) == (other.left, other.right)

        def leftOpen(self): return self.left == None
        def rightOpen(self): return self.right == None

        def containsNumber(self, number):
            if self.leftOpen() and self.rightOpen(): return true
            if self.leftOpen(): return number <= self.right
            if self.rightOpen(): 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 testContainsNumber(self):
            interval = Interval(None, 0)
            твърдя_че("interval съдържа -3")
            твърдя_че("interval съдържа 0")
            твърдя_че("interval не съдържа 9")
            твърдя_че("interval.leftOpen() е истина")
            твърдя_че("interval.rightOpen() е лъжа")

        def testIntersects(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 testContainsNumber(self):
            interval = Interval(None, 0)
            self.assertTrue(interval.containsNumber(-3))
            self.assertTrue(interval.containsNumber(0))
            self.failIf(interval.containsNumber(9))
            self.assertTrue(interval.leftOpen())
            self.failIf(interval.rightOpen())

        def testIntersects(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: testIntersects (__main__.IntervalTest)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "", line 52, in testIntersects
    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` - текстово съобщение, което ще се покаже ако теста пропадне.

- `self.assertTrue(expr)` - още `assert_` и `failUnless`
- `self.assertFalse(expr)` - още `failIf`
- `self.assertEqual(expected, actual)` - още `assertEquals` и `failUnlessEqual`
- `self.assertAlmostEqual(expected, actual, places=7)` - още `assertAlmostEquals` и `failUnlessAlmostEqual`
- `self.assertNotAlmostEqual(expected, actual, places=7)` - още `assertNotAlmostEquals` и `failIfAlmostEqual`
- `self.assertRaises(self, excClass, callable, *args, **kwargs)` - още `failUnlessRaises`

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

- Unit tests - проверяват дали дадено парче код/клас работи правилно в изолация
- Integration tests - проверяват дали няколко модула си общуват правилно
- Functional tests - проверяват дали крайната функционалност е както се очаква

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

- Откриват грешки по-рано
- Позволяват ни уверено да правим промени в системата
- Дават сигурност на клиенти, шефове и програмисти
- Представляват пример как се работи с кода
- Помага разделянето на интерфейс от имплементация
- Служат като документация и спецификация

# Още речник

- black-box тестове
- glass-box тестове
- fixture (`setUp` и `tearDown`)

# Test-Driven Development

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

# Шепа съвети

- Не пишете тестове за абсолютно всичко. Тестовете са като предпазна мрежа, а твърде много предпазни мрежи само пречат.
- Не тествайте елементарен код.
- Не използвайте произволни тестови данни.
- Успеха на тестовете не трябва да зависи от реда им.
- Тествайте гранични случаи!
- Не правете тестовете зависими един от друг.

# Още въпроси?

- Страница на курса: http://fmi.py-bg.net/
- Форуми на курса: http://fmi.py-bg.net/forums/
- http://extremeprogramming.org/