Тесты на устойчивость к перекурам с Python
Как часто хочется продолжить писать программу без нудного тестирования ее работы, ведь кажется, что достаточно убедиться в правильности поведения путем пошагового прохождения по командам в отладчике. Особенно больно писать тесты, связанные с откликом на внешние события, так как приходится их имитировать...запускать отдельные процессы или потоки...
И все это, на первый взгляд, лишь затормаживает вашу работу, но в действительности тесты нужны. Их польза ощущается после возврата к коду программы через некоторое время, когда смотришь, и кажется будто не ты ее писал, настолько все не понятно.
Итак, засучим рукава и продемонстрируем как имитировать внешнее воздействие и проверять корректность отклика на него программы - останов с сохранением состояния и продолжение работы с запомненной позиции.
Задачу будем рассматривать на примере приложения по парсингу веб-страниц, о котором я неоднократно писал ранее (подробнее читай здесь).
Как обычно для написания тестов создадим класс, наследующий unittest.TestCase (подробнее здесь). Первоначальную инициализацию перед каждым тестом будет осуществлять его метод setUp, в котором мы создадим экземпляр класса парсера, заполненный значениями из свойства true_vals_d. Также в классе для каждого теста создаются отдельные поля, в которых находятся правильные значения, сравнивающиеся с выходом программы. Например, словарь с целевыми параметрами после останова приложения (start_stop_vals_d), скачавшего n записей о проведенных поединках в рамках турниров UFC, начиная с заданного (ссылка на него указывается в true_vals_d).
Как я указывал ранее (подробнее читай здесь), для имитации внешнего воздействия можно запустить поток или процесс, который его инициирует (метод click_pause, запускается в тестовом методе test_start_stop).
Ниже привожу код класса с методами и полями, предназначение которых описывается в комментариях к ним:
import unittest
from ufc.ufc_fights_parser import UFCFightsParser
import pickle
import threading
class ScraperTest(unittest.TestCase):
# параметры для инициализации объекта в setUp
true_vals_d={'cur_url':'http://www.ufcstats.com/event-details/fc9a9559a05f2704?',
'events_url':'http://www.ufcstats.com/statistics/events/completed?page=1',
}
# поле для проверки сохранения состояния между
# исключениями или остановами
start_stop_vals_d = {
# останов после стольких записей
# скачаться успеет на одну больше, так как
# пока флаг останова установили очередная запись качается
'parse_num':25,
# ссылка на текущий турнир после 25 записей
'next_url':'http://www.ufcstats.com/event-details/5df17b3620145578?',
# номер страницы с турниром на сайте,
# задается в виде строки для внутрен. манипуляций
'next_page':'2',
# номер записи, по которую докачали включительно
# старт произойдет со следующей
'records_pass_in_page_num':2,
# полный url страницы с турнирами после 25 записей
'next_events_url':'http://www.ufcstats.com/statistics/events/completed?page=2',
# номер турнира на странице ссылок на турниры
'event_ind':1
}
def click_pause(self):
while True:
time.sleep(0.1)
if len(self.ufc_fights.items_list)>=int(self.start_stop_vals_d['parse_num']):
print(len(self.ufc_fights.items_list))
self.ufc_fights.pause_flag=True
break
def test_start_stop(self):
thread = threading.Thread(target=self.click_pause)
thread.start()
try:
self.ufc_fights.start_items_parser()
except:
# сохраняем состояние
with open('params_page_parser', 'wb') as f_w:
tuple_params = self.ufc_fights.save_class_params()
pickle.dump(tuple_params, f_w)
self.assertTrue(os.path.exists('params_page_parser'))
# получаем параметры состояния
with open('params_page_parser', 'rb') as f_r:
saved_d = pickle.load(f_r)
# проверяем правильность запомненных параметров и истинных значений
self.assertEqual(self.start_stop_vals_d['next_url'],saved_d['cur_url'])
self.assertEqual(self.start_stop_vals_d['next_page'],saved_d['cur_page'])
self.assertEqual(self.start_stop_vals_d['records_pass_in_page_num'],
saved_d['records_pass_in_page_num'])
self.assertEqual(self.start_stop_vals_d['next_events_url'],saved_d['events_url'])
self.assertEqual(self.start_stop_vals_d['event_ind'],saved_d['event_ind'])
# инциализируем класс парсера запомненными параметрами
# и проверяем, что очередная запись будет содержать заданное значение
self.ufc_fights.load_class_params(saved_d)
items_hrefs = self.ufc_fights.get_item_hrefs(self.ufc_fights.cur_url,\
self.ufc_fights.tag_container_el,self.ufc_fights.tag_el, self.ufc_fights.delay)
url = items_hrefs[self.ufc_fights.records_pass_in_page_num]
item_params = self.ufc_fights.get_one_item_params(url)
self.assertTrue('Montana' in item_params['Fighter_left'])
def setUp(self):
tag_container_events='tbody,,'
tag_event='a,class,b-link b-link_style_black'
tag_container_el = 'tbody,class,b-fight-details__table-body'
tag_el = 'a,class,b-flag b-flag_style_green'
cur_url = self.true_vals_d['cur_url']
events_url=self.true_vals_d['events_url']
page_param = 'page'
delay = 1
self.ufc_fights = UFCFightsParser(cur_url,events_url,delay,page_param,\
tag_container_events, tag_event,\
tag_container_el,tag_el)
Проведение тестов зачастую отнимает много времени, но эта задача разрешима и избавит вас от ряда хлопот в будущем.
А у вас какие возникали сложности с тестированием? Делитесь в комментариях и вместе обсудим.