February 5, 2021

Тесты на устойчивость к перекурам с 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)

Проведение тестов зачастую отнимает много времени, но эта задача разрешима и избавит вас от ряда хлопот в будущем.

А у вас какие возникали сложности с тестированием? Делитесь в комментариях и вместе обсудим.