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()
Идея
- Добре де... хващам се, че постоянно правя едно и също нещо като робот. Понеже е досадно, лесно ще забравя нещо. Пък и само ми губи времето. Човешката цивилизация не реши ли тоя вид проблеми с някакви машини? Май се казваха компютри?
— Защо просто не си напишеш програма, която да го прави вместо теб?
На хартия (или проектор)
- За всичко съмнително ще пишем сценарий, който да "цъка".
- Всеки сценарий ще изпълнява кода и ще прави няколко твърдения за резултатите.
- Сценариите ще бъдат обединени в групи.
- Пускате всички тестове с едно бутонче.
- Резултатът е "Всичко мина успешно" или "Твърдения 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({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 - текстово съобщение, което ще се покаже ако теста пропадне.
- self.assertTrue(expr)
- self.assertFalse(expr)
- self.assertEqual(expected, actual)
- self.assertAlmostEqual(expected, actual, places=7)
- self.assertNotAlmostEqual(expected, actual, places=7)
- self.assertRaises(self, excClass, callable, *args, **kwargs)
Видове тестове
- Unit tests - проверяват дали дадено парче код/клас работи правилно в изолация
- Integration tests - проверяват дали няколко модула си общуват правилно
- Functional tests - проверяват дали крайната функционалност е както се очаква
За какво ни помагат тестовете
- Откриват грешки по-рано
- Позволяват ни уверено да правим промени в системата
- Дават сигурност на клиенти, шефове и програмисти
- Представляват пример как се работи с кода
- Помага разделянето на интерфейс от имплементация
- Служат като документация и спецификация
За какво не служат тестовете
- Не доказват, че приложението работи
- Не са Quality Assurance
- Не са benchmark
Още речник
- black-box тестове
- glass-box тестове
- fixture (setUp и tearDown)
Документация
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)
- Добави тест
- Пусни всички тестове и виж, че новият се чупи
- Напиши код
- Пусни тестовете и виж че минават успешно
- Подобри кода (refactor)
- Повтаряй
Демо
лишън!
Test-Driven Development (3)
- Подход за писане на код
- Дизайна е базиран върху обратна връзка, не гадаене
- Спестява излишен код -- пишете само каквото ви трябва
- Спестява излишна функционалност
- Продуктивност!
Behaviour-Driven Development
- Теst-Driven Development by Example
- The RSpec Book
- xUnit Test Patterns
Шепа съвети
- Пишете тестове за всичко, което може да се счупи.
- Не тествайте елементарен код.
- Не използвайте произволни тестови данни.
- Успеха на тестовете не трябва да зависи от реда им.
- Тествайте гранични случаи!
- Не правете тестовете зависими един от друг.
Още въпроси?
- Страница на курса: http://fmi.py-bg.net/
- Форуми на курса: http://fmi.py-bg.net/topics
- http://extremeprogramming.org
Още въпроси?
- Пишете ни на fmi@py-bg.net
- Страница на курса: http://fmi.py-bg.net/
- Форуми на курса: http://fmi.py-bg.net/topics
- Курсът в Twitter: http://twitter.com/pyfmi
- Курсът във Facebook: http://www.facebook.com/group.php?gid=104970619536589