05. Автоматизирано тестване

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.assertEqual(
                    Interval(5, 10), Interval(0, 10) & Interval(5, None))
                self.assertEqual(
                    Interval(None, 0), Interval(None, 42) & Interval(None, 0))
                self.assertEqual(
                    Interval(-20, 20), Interval(None, 20) & Interval(-20, None))
                self.assertEqual(
                    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 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 (3)

Behaviour-Driven Development

Шепа съвети

Още въпроси?

Още въпроси?