събота, 17 януари 2009 г.

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


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

Стефан Кънев & Николай Бачийски

11.04.2007г.

Disclaimer

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

Митът

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

Митът v1.1

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

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

  • Заданията винаги се променят.
  • Често се налага един код да се преработва.
  • Писането на код е сложна задача - могат да се направят много грешки.
  • Програмистите са хора - допускат грешки.
  • Или губите ужасно много време в разцъкване след всяка промяна, или рискувате да вкарате бъгове в приложението, които ще бъдат открити твърде късно.
  • Промяна в един модул в единия край на системата като нищо може да причини грешка в другия, без въобще да усетите.

Занятието


class Programmer(object):
...
def makeChange(self, project, change):
self.openAssociatedFiles(project, change)
while True:
self.attemptChange(change)
project.start()
result = self.clickAroundAndTest(project)
project.stop()
if result.sucessful(): break
self.commitCode(project)
self.hopeEverythingWentOK()

Идея

Това е автоматизирана задача. Всеки път повтаряме едно и също, отнема ни много време, уморява ни и има голям шанс да пропуснем нещо или да допуснем грешка. Защо вместо това не напишем малка програмка, която проверява дали кодът ни работи като хората? Така не просто ще имаме хубави критерии за кода, но и всеки друг ще може да проверява дали неговата промяна в модул А не е довела до грешка в нашия модул Б. Вместо да губим време да цъкаме всеки път, ще пускаме всички тестове автоматично и една команда, няма да въвеждаме никакви параметри и просто ще чакаме един от двата отгорова - "Всички тестове минаха усешно" или "Имаше грешка в тест Х".

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


class Interval(object):

def __init__(self, left, right):
self.left, self.right = left, 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 __max(self, a, b): max(a, b) if not None in [a, b] else None
def __min(self, a, b): min(a, b) if not None in [a, b] else None

def insersect(self, other):
left = self.__max(self.left, other.left)
right = self.__max(self.right, other.right)
return Interval(left, right)

Идеята...


class IntervalTest:
def testContainsNumber(self):
interval = Interval(None, 0)
test("interval съдържа -3")
test("interval съдържа 0")
test("interval не съдържа 9")
test("interval.leftOpen() е истина")
test("interval.rightOpen() е лъжа")

def testIntersects(self):
test("сечението на [0, 10] със [5, None] е [5, 10]")
test("сечението на [None, 0] със [None, 42] е [None, 0]")
test("сечението на [None, 20] със [-20, None] е [-20, 20]")

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

    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).intersect(Interval(5, None)))
self.assertEquals(
Interval(None, 0),
Interval(None, 42).intersect(Interval(None, 0)))
self.assertEquals(
Interval(-20, 20),
Interval(None, 20).intersect(Interval(-20, None)))

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

...и резултата

.F
======================================================================
FAIL: testIntersects (__main__.GoodieListTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "./interval_test.py", line 16, in testCompact
self.assertEquals(
Interval(None, 0),
Interval(None, 42).intersects(Interval(None, 0)))
AssertionError: Interval(None, 0) != Interval(0, 0)

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)

Структура на тестовете

  • test case - група сценарии които споделят нещо общо помежду си.
  • test method - единичен сценарий, който проверява нещо конкретно.
  • assertation - единична проверка по време на изпълнение на сценария

Структура на тестовете в Python

  • test case - клас, наследяващ unittest.TestCase
  • test method - метод във въпросния клас, чието име започва с test и изпълнява поне една асертация.
  • assertation - извикване на self.assert* в тестовия метод.

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

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

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

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

Няма коментари:

Публикуване на коментар