Журнал LinuxFormat - перейти на главную

LXF171:Обеспечение качества ПО

Материал из Linuxformat
Перейти к: навигация, поиск

Содержание

Тес­ти­ро­ва­ние: Да­ешь ка­че­ст­во ПО

Джо­но Бэ­кон ис­сле­ду­ет мир ав­то­ма­ти­че­ско­го и руч­но­го тес­ти­ро­ва­ния, что­бы по­мочь вам сде­лать свое при­ло­же­ние проч­ным, как ска­ла. [[Файл: |left | thumb|100px|Наш эксперт Джо­но Бэ­кон — ме­нед­жер со­об­ще­ст­ва Ubuntu, ав­тор The Art Of Community [Ис­кус­ст­во Со­об­ще­ст­ва] и ос­но­ва­тель еже­год­ной встре­чи Community Leadership Summit.]] Ка­че­­ст­во — клю­че­вой фак­тор для лю­бой про­грам­мы. Не­важ­но, на­сколь­ко со­вре­мен­ны ва­ши функ­ции, на­сколь­ко впе­чат­ля­ет ваш ин­тер­фейс или на­сколь­ко уни­каль­но ва­ше при­ло­же­ние: ес­ли оно не­на­деж­но ра­бо­та­ет, им пе­ре­ста­нут поль­зо­вать­ся.

Од­на­ко ка­че­­ст­во важ­но не толь­ко для обес­пе­че­ния поль­зо­ва­те­лю при­ят­ной ра­бо­ты — оно важ­но так­же для то­го, что­бы ва­ше со­об­ще­ст­во чув­ст­во­ва­ло се­бя сча­ст­ли­вым и пре­дан­ным ва­ше­му при­ло­же­нию. Пло­хое ка­че­­ст­во от­ра­жа­ет­ся на всех, кто от­стаи­вает ин­те­ре­сы со­об­ще­ст­ва, а вам ведь не на­до, что­бы ва­ше со­об­ще­ст­во по­сто­ян­но по­лу­ча­ло жа­ло­бы на пло­хое ка­че­­ст­во про­грам­мы и на ошиб­ки в ней.

Соз­да­ние вы­со­ко­ка­че­­ст­вен­но­го ПО оз­на­ча­ет по­ни­ма­ние про­цес­сов Quality Assurance (QA, Обес­пе­че­ние ка­че­­ст­ва). Не­ко­то­рые из вас, воз­мож­но, уже зна­ко­мы с QA, и ас­со­ции­ру­ют его с от­сле­жи­ва­ни­ем оши­бок и тес­ти­ро­ва­ни­ем про­грамм. В це­лом это пра­виль­но, но эф­фек­тив­ное QA в мень­шей сте­пе­ни от­но­сит­ся к со­об­ще­ни­ям о де­фек­тах и от­сле­жи­ва­нию их, а в боль­шей сте­пе­ни — к обес­пе­че­нию ка­че­­ст­ва ва­ше­го ПО. Ины­ми сло­ва­ми, луч­ше по­ста­рать­ся обес­пе­чить вы­со­кое ка­че­­ст­во вы­пус­кае­мо­го ПО, что­бы в нем во­об­ще не бы­ло оши­бок, а не от­лич­ные про­цес­сы для со­об­ще­ния об об­на­ру­жен­ных ошиб­ках.

Что­бы обес­пе­чить ка­че­­ст­во, тре­бу­ет­ся эф­фек­тив­ное тес­ти­ро­ва­ние ко­да, из­го­няю­щее из ко­да как мож­но боль­ше оши­бок. И эта за­да­ча на­мно­го слож­нее, чем мож­но ожи­дать. С од­ной сто­ро­ны, все­гда мож­но про­тес­ти­ро­вать раз­ные час­ти ко­да и про­ве­рить, ра­бо­та­ют ли они, как по­ло­же­но; но как быть в тех слу­ча­ях, ко­гда код ра­бо­та­ет в не­обыч­ных ус­ло­ви­ях или с не­обыч­ны­ми дан­ны­ми? При этом воз­ни­ка­ют ху­же все­го от­сле­жи­вае­мые про­бле­мы, а так­же гон­ки, ко­гда раз­ные вет­ки ко­да ис­пол­ня­ют­ся в раз­ное вре­мя.

Что­бы до­бить­ся на­шей це­ли, обес­пе­че­ния ка­че­­ст­ва, луч­шим ме­то­дом бу­дет при­ме­нить мно­го­гран­ный под­ход к тес­ти­ро­ва­нию. В идеа­ле на­до про­тес­ти­ро­вать наш код по­блоч­но, а так­же про­тес­ти­ро­вать все функ­ции с точ­ки зре­ния поль­зо­ва­те­ля. Ог­ра­ни­чив­шись од­ной из этих за­дач, долж­ный уро­вень ка­че­­ст­ва вы не обес­пе­чи­те; а вы­пол­нив обе, ста­не­те го­раз­до бли­же к нир­ва­не ста­биль­но­сти ПО.

По­блоч­ное тес­ти­ро­ва­ние

Са­мая важ­ная фор­ма тес­ти­ро­ва­ния, ко­то­рую нуж­но встро­ить в ва­ше при­ло­же­ние, это блоч­ное тес­ти­ро­ва­ние. Ог­ром­ное ко­ли­че­­ст­во про­грамм на­пи­са­но на функ­цио­наль­ных объ­ект­но-ори­ен­ти­ро­ван­ных язы­ках про­грам­ми­ро­ва­ния, в ко­то­рых код раз­би­ва­ет­ся на функ­ции мно­го­крат­но­го ис­поль­зо­ва­ния, ко­то­рые слу­жат спе­ци­аль­ным це­лям. На­при­мер, у вас, воз­мож­но, есть функ­ция, ко­то­рая пи­шет файл на диск, или кон­вер­ти­ру­ет дан­ные в дру­гой тип, или воз­вра­ща­ет web-стра­ни­цу с web-сер­ве­ра. По­блоч­ное тес­ти­ро­ва­ние раз­ра­бо­та­но для то­го, что­бы все эти раз­но­род­ные функ­ции ра­бо­та­ли как по­ла­га­ет­ся и до­би­ва­лись же­лае­мо­го ре­зуль­та­та.

Об­щая фи­ло­со­фия по­блоч­но­го тес­ти­ро­ва­ния за­клю­ча­ет­ся в том, что у вас есть тест для ка­ж­дой функ­ции в ва­шем при­ло­же­нии. Во мно­гих про­грамм­ных про­ек­тах но­вую функ­цию не при­мут в ко­до­вую ба­зу про­ек­та, по­ка не бу­дет про­тес­ти­ро­ван со­от­вет­ст­вую­щий блок. Ана­ло­гич­но, но­вые функ­ции и по­прав­ки не по­па­дут в про­грам­му, по­ка не прой­дут блоч­но­го тес­ти­ро­ва­ния. Это обес­пе­чи­ва­ет на­деж­ный спо­соб про­вер­ки, что но­вые кус­ки ко­да не­ча­ян­но не на­ру­шат уже отлаженные функ­ции ва­шей программы.

Есть мно­же­ст­во раз­ных сред для на­пи­са­ния блоч­ных тес­тов, и не­уди­ви­тель­но, что мно­гие из них от­но­сят­ся имен­но к то­му язы­ку, на ко­то­ром на­пи­сан код. В этой ста­тье я по­ка­жу вам, как ис­поль­зо­вать мо­дуль unittest, яв­ляю­щий­ся ча­стью Python; од­на­ко струк­ту­ра соз­да­ния тес­та мо­жет ис­поль­зо­вать­ся в боль­шин­ст­ве дру­гих тес­то­вых сред.

Пре­ж­де чем при­сту­пать к соз­да­нию тес­та, пред­по­ло­жим, что у нас есть про­стой класс со сле­дую­щи­ми функ­ция­ми:

class MyClass():

def return_true(self):

return True

def get_version(self):

version = “1.0”

return version

def get_file(self, fileloc):

try:

with open(fileloc) as f: return True

except IOError as e:

return False

Функ­ций здесь три:

» return_true() В та­ком ви­де это са­мая бес­полез­ная из всех ко­гда-ли­бо на­пи­сан­ных функ­ций; но вы пред­ставь­те, что она де­ла­ет не­что осмысленное и за­тем воз­вра­ща­ет True.

» get_version() Эта функ­ция про­сто воз­вра­ща­ет те­ку­щую вер­сию ПО в ка­че­­ст­ве стро­ко­вой пе­ре­мен­ной. Она мо­жет быть по­лез­на при на­пи­са­нии API, что­бы кли­ент взял пра­виль­ную вер­сию API.

» get_file() Эта функ­ция про­ве­ря­ет, су­ще­ст­ву­ет ли ука­зан­ный файл.

Все эти функ­ции воз­вра­ща­ют дан­ные, и нам на­до на­пи­сать тест бло­ка, про­ве­ряю­щий, что вер­ну­лись пра­виль­ные дан­ные. Ес­ли воз­вра­ща­ют­ся дру­гие дан­ные (на­при­мер, False вме­сто True), зна­чит, блоч­ный тест не про­хо­дит.

Да­вай­те сна­ча­ла до­ба­вим ут­вер­жде­ния import, ко­то­рые мы бу­дем ис­поль­зо­вать в этой ста­тье, а по­том до­ба­вим при­ве­ден­ный вы­ше класс:

import unittest

import tempfile

import os, sys, shutil

class MyClass():

def return_true(self):

return True

def get_version(self):

version = “1.0”

return version

def get_file(self, fileloc):

try:

with open(fileloc) as f: return True

except IOError as e:

return False

Теперь создайте свой первый тест, в данном случае — для функции return_true(). Под кодом, который мы только что добавили (код см. выше), вставьте следующее:

class Tests(unittest.TestCase):

def test_return_true(self):

myclass = MyClass()

value = myclass.return_true()

self.assertTrue(value)

if __name__ == ‘__main__’:

unittest.main()

Здесь мы создаем новый класс unittest под названием Tests. Внутри этого класса мы создаем блочный тест для каждой функции. Как ви­ди­те, мы добавили наш первый блочный тест, под названием test_return_true().

Для начала мы создаем экземпляр класса MyClass, который тестируется, а затем запускаем функцию и пишем результат в value.

Следующий наш шаг — про­вер­ка, являются ли данные в value тем, что мы ожидали. С этой це­лью мы выполняем утверждение. Утверждение проверяет, соответствует ли пе­ре­да­вае­мая ему величина тому, что оно ожидает. В этом тесте мы используем assertTrue(), являющееся частью библиотеки unittest, чтобы проверить, является ли value истинной (True), как мы рассчитываем.

Затем в кон­це исходника мы запускаем функцию main() модуля unittest, чтобы провести тесты.

Что­бы запустить свои тесты и узнать, пройдут ли они, просто выполните скрипт, и вы должны увидеть следующее:

jono@forge:~/Desktop$ python tests.py .


Ran 1 test in 0.000s

OK

Здесь мы запустили один тест, и он прошел отлично. ‘.’ над линией показывает, что тест пройден. Чтобы проверить, не провалился ли тест, измените return True на return False в функции return_true() и снова запустите скрипт. Теперь на экране должно появиться

jono@forge:~/Desktop$ python tests.py

F

======================================
=======

FAIL: test_return_true (__main__.Tests)


Traceback (most recent call last):

File “tests.py”, line 35, in test_return_true

self.assertTrue(value)

AssertionError: False is not true


Ran 1 test in 0.000s

FAILED (failures=1)

Здесь тест не пройден, и указана причина провала.

Давайте напишем второй тест, чтобы проверить, возвращает ли get_version() правильную версию. В этом случае нам не нужно тестировать кон­крет­но версию 1.0, поскольку версии регулярно из­ме­ня­ют­ся. Вместо этого сле­ду­ет убедиться, что функция возвращает переменную, например, 1.0 или 1.5, и мы будем считать эту переменную соответствующим номером версии (поскольку никакой другой код не вносит данные в эту функцию).

Чтобы добавить дан­ный тест, вставьте та­кую функцию в класс Tests:

def test_get_version(self):

myclass = MyClass()

version = myclass.get_version()

self.assertTrue(isinstance(version, basestring))

Здесь мы снова создали экземпляр класса MyClass, запустили get_version() и вывели результат в version.

Теперь нам надо протестировать, является ли version переменной. Для этого мы используем isinstance(), чтобы проверить, относится ли version к формату Basestring, и, если это так, то вернется True; затем мы проверим это в функции assertTrue(), чтобы вернуть результат утверждения. Сно­ва запустив скрипт, мы увидим:

jono@forge:~/Desktop$ python tests.py

..


Ran 2 tests in 0.000s

OK

Здесь видно, что проведено два теста, и над ли­ни­ей поставлено по точке за каждый удачно пройденный тест.

И снова, если вы хотите, чтобы тест не про­шел, от­ре­дак­ти­руй­те исходную функцию, на сей раз изменив “1.0” на 1.0 (удалите кавычки, превратив единицу в число вместо переменной) и перезапустите скрипт, чтобы увидеть провал.

Для нашего финального теста я хочу поговорить о важных частях написания блочных тестов — подготовке и закрытии.

Вы, возможно, заметили, что наши функции не работают с реальными данными. Например, если нужно провести блочный тест, чтобы увидеть, содержит ли определенного типа файл определенный тип данных, как провести этот тест и узнать, что данные, которые мы вводим в функцию — это данные, соответствующие тому типу, на который мы рассчитываем? Конечно, данные могут быть другими, что может послужить причиной провала теста, даже если данные были приемлемы.

Чтобы решить эту проблему, модуль unittest (и многие другие среды тестирования) позволяет запускать эквивалент создателя классов, при­ме­ни­мый для создания пробных данных для теста. Точно так же есть эквивалент де­ст­рук­то­ра классов, который можно использовать для последующего удаления этой тестовой среды. Рассмотрим нашу последнюю функцию, для которой мы хотим написать тест:

def get_file(self, fileloc):

try:

with open(fileloc) as f: return True

except IOError as e:

return False

В данном случае функция проверяет, существует ли файл, и если да, то возвращает True; в противном случае появляется ошибка IOError, и функция возвращает False. Для эффективного тестирования этой функции нам нужно знать, действительно ли существует файл, который мы ей передаем.

Вот тут пригодится папка /tmp в вашем компьютере. Мы автоматиче­ски создадим несколько файлов в /tmp, чтобы точно знать те файлы, которые наш блочный тест будет использовать в каче­стве исходных данных.

Для этого добавьте следующую функцию в ваш класс Tests:

def setUp(self):

self.temp_path = ‘/tmp/testingtemp/’

if not os.path.exists(self.temp_path): os.mkdir(self.temp_path)

for i in range(1, 6):

file = open(os.path.join(self.temp_path, str(i) + “.txt”), ‘w’)

file.write(‘foo’)

file.close()

Здесь мы создаем нашу функцию setup (эквивалент создателя классов). Для этого создается функция под названием setUp(), определяется местоположение в /tmp для сохранения наших файлов, проверяется, существует ли эта директория, и затем создается пять небольших текстовых файлов под названием 1.txt, 2.txt, и т. д.

Когда мы запускаем наши тесты, функция setUp() запускается перед выполнением любого теста. По завершении этой функции у нас будет пять текстовых файлов в /tmp/Testingtemp, которые мы используем в тестах. Это обеспечит готовность нашей тестовой среды перед запуском тестов.

Давайте теперь создадим тест:

def test_get_file(self):

myclass = MyClass()

value = myclass.get_file(“/tmp/testingtemp/1.txt”)

self.assertTrue(value)

Здесь мы создаем экземпляр класса MyClass, запускаем функцию get_file() и передаем ей один из файлов, созданных с помощью setUp(). Техниче­ски нам нужно создать один текстовый файл, но мне показалось ве­се­лее создать пять. Затем тест проверит, является ли величина, возвращаемая из get_file(), истинной (True). Если это так, тест пройден. Запустив скрипт, мы увидим:

jono@forge:~/Desktop$ python tests.py

...


Ran 3 tests in 0.001s

OK

Как видите, все три теста пройдены успешно.

Удаление тестовых данных

Хотя /tmp периодиче­ски вы­чи­ща­ет­ся системой, и наши тестовые данные будут удалены, хо­ро­шим то­ном счи­та­ет­ся пре­ду­смот­реть функцию, очи­щаю­щую от тестовых данных. Для этого создадим функцию tearDown() в классе Tests:

def tearDown(self):

temp_path = ‘/tmp/testingtemp/’

shutil.rmtree(self.temp_path)

Эта функция просто удаляет директорию из /tmp. Теперь, запустив тесты и заглянув в /tmp после их завершения, вы увидите, что тестовых данных там нет.

Конечно, есть много других функций и возможностей в модуле unittest, но я советую заглянуть в руководство пользова­теля на http://docs.python.org/release/2.6.6/library/unittest.html, где вы найдете более подробную информацию, или в документацию тестовой среды, которой вы пользуетесь.

Тестирование функций

Блочные тесты являются важной частью разработки ПО, и я настоятельно рекомендую вам обзавестись пакетом блочных тестов для ваших приложений, желательно с тестом для каждой функции. Однако блочные тесты — это только проверка функциональных возможностей кода. И они совершенно не выявляют неожиданных результатов при ра­бо­те приложения.

Среди примеров таких неожиданных результатов могут быть:

» рендеринг ошибок в графиче­ских приложениях;

» сломанные или не отвечающие графиче­ские виджеты;

» проблема недоступности сетевого соединения;

» текст в графиче­ском приложении занимает слишком много места на экране.

» Приложения работают со сбоями или с ошибками.

» Проблемы интеграции приложения с другими компонентами системы (например, с темами).

Любой из этих сценариев может возникать при благополучно пройденном наборе блочных тестов. И это оставляет нам две возможности тестирования функций: тесты рабочего стола и ручные тесты.

Тесты рабочего стола — это автоматиче­ские тесты, имитирующие щелчки мышью в приложении и оценивающие результаты этих щелчков на предмет их соответствия ожиданиям. Есть два основных подхода к проведению этих тестов:

» Уровень доступности — эти тесты создаются инициирую­щими событиями с помощью среды доступности на рабочем столе (той же среды, которая используется инструментами доступа, например, программами для чтения с экрана).

» Снимки с экрана — эти тесты основаны на том, что делается серия скриншотов, и затем сравниваются функции приложения с частями скриншота этого приложения (например, панель инструментов приложения соответствует панели инструментов на скриншоте).

И хотя обе эти техники бы­ва­ют полезны, они предполагают наличие не­ких технологий (например, среды доступности или настроенного набора скриншотов, которые соответствуют теме рабочего стола). По­это­му от­сы­лаю вас к инструментам, используемым для опробования этих подходов (в наборе инструментов Desktop Testing Tools), а вместо этого мы сконцентрируемся на ручных тестах, при­ло­жи­мых ко всему.

Ручное тестирование

Ручные тесты — это подборка небольших предписывающих инструкций, которые мы просим выполнить пользователя для соз­дания ожидаемого результата, а затем просим пользователя сравнить этот результат с долж­ным.

Инструменты для предоставления пользователю ручных тестов (например, Ubuntu test tracker) в первую очередь предназначены для перечисления тестов и предоставления места для сохранения результатов тестирования. Однако в реальности вы можете использовать для этого другие инструменты, например, wiki или электронную таблицу для хранения результатов.

Создание ручных тестов может показаться не слож­нее, чем написание нескольких инструкций, однако вам надо подойти к этому более методично, чтобы вы точно смогли протестировать все необходимые части вашего приложения, и чтобы каждый тест работал как надо и выдавал те результаты, которых вы и ожидали. Мы не хотим, чтобы тест провалился из-за того, что ваши инструкции неточны и пользователь их недопонял и нажал не там.

Первый шаг в написании отличного ручного теста — это определение точного списка того, что на­до тестировать. Например, для текстового редактора рабочего стола нужно будет протестировать:

» операции с файлами (загрузка/сохранение/перезапись);

» добавление, редактирование, удаление и перемещение текста;

» такие функции, как проверка правописания, поиск, замена, статистика по словам, и т. д.

Составив список требований, можете приступать к написанию тестов, которые будут выполнять пользователи.

Но пре­ж­де чем за­ба­ра­ба­нить по клавиатуре, обдумайте, какие основные тесты вам нужны — те, что отсутствуют в виде блочных тестов или в иной форме тестирования. Когда мы просим пользователей провести ручное тес­ти­ро­ва­ние, мы не рассчитываем, что они про­си­дят за тес­та­ми четыре часа; это весьма скоро им надоест. Ку­да прак­тич­нее попросить их уделить 20 минут и протестировать самые проблемные или рискованные области вашего приложения, чтобы и нужное тестирование обеспечить, и не ввергнуть пользователя в тоску.

Помня о том, какие вам нужны тесты, создайте в текстовом редакторе новый документ, чтобы написать их, и присвойте каждому тесту номер и идентификатор. Например:

ED-001 file-loading

ED-002 file-editing

ED-003 file-saving

Для каждого теста добавьте описание того, что он делает. Описание должно быть высокоуровневым, но подробным настолько, чтобы быть понятным людям, незнакомым с тестом и приложением. Например:

ED-001 file-loading Загружает текстовый файл в редактор, готовый к редактированию.

ED-002 file-editing Редактирует текстовый файл посредством ввода, удаления и перемещения текста.

ED-003 file-saving Со­хра­ня­ет текст в но­вый файл.

За­тем за­до­ку­мен­ти­руй­те все настройки, которые должен сделать пользователь перед запуском теста. Например, для ED-001 — должен ли файл, который он загружает, быть в определенной кодировке и должен ли он загружаться с жесткого диска или с устройства или из сетевого ресурса? Это должно быть ясно указано. Например:

Подготовка: используйте TextEditor 1.0 и загрузите текст в формате UTF-8 с локального жесткого диска.

Теперь для каждого теста напишите набор действий, объясняющих, как проводить тест. Например, для ED-001:

1 Щелкните по пункту меню File.

2 Щелкните пункт Открыть... внутри меню File.

3 Используя выбор файла, выберите текстовый файл (текстовый файл показан небольшим значком с блокнотом и должен иметь расширение .txt).

Каждый тест дол­жен включать не более 10 – 15 действий; если их будет больше, это просто убьет пользователя.

Теперь внятно и четко опишите ожидаемый результат теста. Например, для ED-001:

Результат: Тек­сто­вый файл загружается, и весь текст отображается в текстовом режиме со всеми пе­ре­во­да­ми строки и возвратами ка­рет­ки.

Получившийся у вас готовый тест должен выглядеть приблизительно так:

ED-001 file-loading Загрузка текста в редактор, готовый к редактированию.

Подготовка: используйте TextEditor 1.0 и загрузите текст в формате UTF-8 с локального жесткого диска.

1 Щелкните по пункту в меню File.

2 Щелкните пункт Открыть… в меню File.

3 Ис­поль­зуя вы­бор фай­ла, вы­бе­ри­те тек­сто­вый файл (тек­сто­вый файл по­ка­зан не­боль­шим знач­ком с блок­но­том и дол­жен иметь рас­ши­ре­ние .txt).

Результат: Текстовый файл загружается и весь текст отображается в текстовом режиме со всеми новыми строками, и носитель возвращается.

Затем пользователь должен сообщить о том пройден ли тест (PASS) или не пройден (FAIL), когда он получит результат, следуя всем перечисленным инструкциям. Поздравляем, теперь у вас есть ручной тест! |


Персональные инструменты
купить
подписаться
Яндекс.Метрика