Тестирование веб-сайта с помощью скрейперов

Когда вы работаете над веб-проектами, которые включают много разных технологий, часто проверяют только серверную часть. Большинство языков программирования, таких как Python, имеют инструменты для тестирования, но часть, с которой работают пользователи (фронтенд), редко тестируется автоматически.

Проблема в том, что веб-сайты состоят из множества разных языков разметки и программирования. Вы можете тестировать части вашего JavaScript, но это не поможет, если HTML изменился так, что JavaScript не работает как задумано, даже если сам код исправен.

Тестирование фронтенда часто не уделяется должного внимания или оставляется для начинающих программистов с простым списком задач и системой отслеживания багов. Но если потратить немного времени заранее, можно заменить список задач набором тестов и использовать веб-скрейперы вместо проверки вручную.

Представьте: разработка, управляемая тестированием, для веб-разработки. Ежедневные тесты для проверки функционирования всех частей веб-интерфейса. Набор тестов выполняется каждый раз, когда кто-то добавляет новую функцию на сайт или изменяет позицию элемента. Эта статья охватывает основы тестирования и способы тестирования всех видов веб-сайтов, от простых до сложных, с использованием веб-скрейперов на базе Python.

Введение в тестирование

Если вы ещё не пробовали писать тесты для своего кода, самое время начать. Когда у вас есть набор тестов, которые можно выполнить, чтобы убедиться, что ваш код работает так, как вы ожидаете (по меньшей мере, для той части кода, для которой тесты написаны), это поможет вам сэкономить время и избежать лишних переживаний. Также это упрощает процесс добавления новых функций и обновлений.

Что такое модульные тесты?

Слова «тест» и «модульный тест» часто используются как синонимы. Когда программисты говорят о «написании тестов», они обычно имеют в виду «написание модульных тестов». В других случаях, когда говорят о модульных тестах, на деле могут писать совсем другие тесты.

Хотя определения и методы могут отличаться в разных компаниях, модульный тест обычно имеет следующие характеристики:

  • Тестирует один аспект функциональности:
    Каждый модульный тест проверяет один аспект работы компонента. Например, тест может проверять, что при попытке снять отрицательную сумму долларов со счета в банке выдается соответствующее сообщение об ошибке.
  • Группировка тестов:
    Часто модульные тесты, тестирующие один компонент, группируются в один класс. Например, тест на снятие отрицательной суммы может быть рядом с тестом на поведение при превышении лимита снятия средств со счета.
  • Независимость выполнения:
    Каждый модульный тест может быть выполнен полностью независимо. Все необходимые подготовительные и завершающие действия должен обрабатывать сам тест. Тесты не должны влиять на успех или провал других тестов и должны успешно выполняться в любом порядке.
  • Содержание утверждений:
    Каждый модульный тест обычно содержит хотя бы одно утверждение. Например, тест может утверждать, что результат сложения 2 + 2 равен 4. Иногда тест может содержать только состояние ошибки, например, он будет считаться неудачным, если возникает исключение, и успешным по умолчанию, если все проходит гладко.
  • Отделение тестов от основного кода:
    Хотя модульные тесты используют и импортируют тестируемый код, они обычно находятся в отдельных классах и каталогах.

Хотя можно написать много других типов тестов (например, интеграционные тесты и тесты проверки), этот раздел в основном сосредоточен на модульном тестировании. Модульные тесты стали очень популярны, особенно с развитием методики разработки через тестирование (test-driven development). Их легко использовать в качестве примеров из-за длины и гибкости, а Python имеет встроенные возможности для модульного тестирования, как вы увидите в следующем разделе.

Python unittest

Модуль unittest в Python помогает тестировать ваш код. Вы создаёте тесты как методы внутри класса, который наследуется от unittest.TestCase. Основные моменты, которые нужно знать:

  • Подготовка и завершение (setUp и tearDown): Эти методы используются для настройки перед тестом и для очистки после теста. Они полезны, когда вам нужно подготовить данные или освободить ресурсы.
  • Утверждения (Asserts): Вы используете утверждения, чтобы проверить, верны ли ваши предположения в коде. Например, self.assertEqual(4, total) проверяет, что total равно 4.
  • Автоматическое распознавание тестов: Методы, названные с test_ в начале, автоматически считаются тестами и запускаются, когда вы вызываете unittest.main().

Ниже приведён простой пример модульного теста, который проверяет, что 2 + 2 = 4, на Python:

import unittest

class TestAddition(unittest.TestCase):
    def setUp(self):
        print('Setting up the test')

    def tearDown(self):
        print('Tearing down the test')

    def test_twoPlusTwo(self):
        total = 2 + 2
        self.assertEqual(4, total)

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

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

Результат выполнения теста из командной строки будет выглядеть так:

Setting up the test
Tearing down the test
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

Это означает, что тест выполнен успешно, и действительно 2 + 2 равно 4.

Запуск unittest в Jupyter Notebooks

В Jupyter Notebooks работа с модульными тестами немного отличается от обычного использования Python-скриптов. В классических Python-скриптах для запуска тестов часто используется конструкция:

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

Эта строка говорит Python, что код нужно выполнить, если скрипт запущен как главный файл, а не подключён как модуль. В Jupyter Notebooks условие `if __name__ == ‘__main__’` выполняется не так, как в обычных скриптах, из-за особенностей среды.

В Jupyter Notebooks для запуска тестов используется немного изменённая версия:

if __name__ == '__main__':
    unittest.main(argv=[''], exit=False)

Почему используется argv=[''] и exit=False:

  1. <strong>argv=['']</strong>:
    Jupyter Notebooks передаёт свои аргументы командной строки в скрипты, и это может вызвать ошибки при использовании `unittest.main()`, который ожидает аргументы, специфичные для тестирования. Чтобы избежать этого, мы переопределяем `argv` (список аргументов командной строки), задавая его как [''], где первый и единственный элемент — пустая строка. Это говорит unittest, что нет дополнительных аргументов командной строки, которые нужно обрабатывать.
  2. <strong>exit=False</strong>:
    По умолчанию, после выполнения всех тестов, `unittest.main()` завершает Python-процесс с помощью sys.exit(). В Jupyter это приведёт к остановке ядра (kernel). Чтобы предотвратить выход из Python, используется exit=False, что позволяет ядру продолжить работу после выполнения тестов.

Использование %reset:

В Jupyter Notebooks есть магическая команда %reset, которая сбрасывает память, удаляя все созданные пользователем переменные. Это полезно, потому что без этого каждый модульный тест будет содержать методы из всех предыдущих тестов, если они наследуют unittest.TestCase. Это может привести к тому, что каждый тест будет выполнять методы из всех предыдущих тестов!

%reset

При использовании `%reset` Jupyter спросит пользователя, действительно ли он хочет сбросить память. Нужно ввести `y` и нажать Enter, чтобы подтвердить.

Пример использования в Jupyter Notebook:

import unittest

class TestAddition(unittest.TestCase):
    def setUp(self):
        print('Setting up the test')

    def tearDown(self):
        print('Tearing down the test')

    def test_twoPlusTwo(self):
        total = 2 + 2
        self.assertEqual(4, total)

if __name__ == '__main__':
    unittest.main(argv=[''], exit=False)

Процесс по шагам:

  1. Написание теста: Вы определяете тесты в классе, наследуемом от unittest.TestCase.
  2. Запуск теста: Вы используете изменённый `unittest.main()` для запуска тестов.
  3. Сброс состояния: После тестирования может потребоваться `%reset` для очистки всех переменных и избегания конфликтов между тестами.

Тестирование Wikipedia

Тестирование фронтенда вашего веб-сайта (за исключением JavaScript, который мы рассмотрим позже) можно выполнить, объединив библиотеку unittest Python с веб-скрейпером. Вот как это можно сделать на примере тестирования страницы Wikipedia:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import unittest

class TestWikipedia(unittest.TestCase):
    bs = None

    @classmethod
    def setUpClass(cls):
        url = 'http://en.wikipedia.org/wiki/Monty_Python'
        TestWikipedia.bs = BeautifulSoup(urlopen(url), 'html.parser')

    def test_titleText(self):
        pageTitle = TestWikipedia.bs.find('h1').get_text()
        self.assertEqual('Monty Python', pageTitle)

    def test_contentExists(self):
        content = TestWikipedia.bs.find('div', {'id': 'mw-content-text'})
        self.assertIsNotNone(content)

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

Объяснение кода:

  1. Импорт необходимых модулей:
    • urlopen из urllib.request для открытия URL.
    • BeautifulSoup из bs4 для парсинга HTML.
    • unittest для написания и выполнения тестов.
  2. Определение класса теста:
    • class TestWikipedia(unittest.TestCase): создаёт новый тестовый класс, наследуя от unittest.TestCase.
  3. Переменная класса:
    • bs = None — это переменная класса, которая будет хранить разобранный HTML документ, чтобы использовать его в различных тестах.
  4. Метод setUpClass:
    • @classmethod указывает, что следующий метод относится ко всему классу, а не к отдельным его экземплярам.
    • def setUpClass(cls): — метод, который выполняется один раз перед всеми тестами. Здесь мы загружаем и парсим страницу Wikipedia.
    • url = 'http://en.wikipedia.org/wiki/Monty_Python' — URL страницы, которую мы тестируем.
    • TestWikipedia.bs = BeautifulSoup(urlopen(url), 'html.parser') — загружаем HTML по указанному URL и разбираем его с помощью BeautifulSoup.
  5. Методы тестирования:
    • def test_titleText(self): — тест, который проверяет, что текст в теге <h1> соответствует «Monty Python».
      • pageTitle = TestWikipedia.bs.find('h1').get_text() — находим первый тег <h1> и получаем его текст.
      • self.assertEqual('Monty Python', pageTitle) — утверждаем, что полученный текст должен быть «Monty Python».
    • def test_contentExists(self): — тест, который проверяет наличие дива с id='mw-content-text'.
      • content = TestWikipedia.bs.find('div', {'id': 'mw-content-text'}) — находим <div> с id="mw-content-text".
      • self.assertIsNotNone(content) — утверждаем, что такой элемент существует.
  6. Запуск тестов:
    • if __name__ == '__main__': — проверка, что скрипт запущен как главный модуль.
    • unittest.main() — функция, которая запускает все тесты в классе.

В этом разделе мы рассмотрим, как тестировать веб-страницу, используя Python и его возможности для модульного тестирования. Мы создадим два теста: один проверит, что заголовок страницы соответствует ожидаемому значению «Monty Python», а второй — что на странице есть блок контента.

Важно отметить, что содержимое страницы загружается только один раз, и объект bs, который содержит разобранный HTML-код страницы, используется общим для всех тестов. Это достигается с помощью функции setUpClass, определённой в unittest. Эта функция выполняется один раз перед всеми тестами в классе, в отличие от setUp, которая выполняется перед каждым тестом. Использование setUpClass вместо setUp позволяет избежать ненужных загрузок страницы: вы загружаете содержимое один раз и можете провести множество тестов на этом содержимом.

Основные различия между setUpClass и setUp:

  • setUpClass — это статический метод, который «принадлежит» самому классу и имеет доступ к глобальным переменным класса. Он выполняется один раз перед началом всех тестов в классе.
  • setUp — это метод экземпляра, который принадлежит конкретному экземпляру класса. Он выполняется перед каждым тестом и может устанавливать атрибуты на self — текущем экземпляре класса.

Поэтому setUp может устанавливать атрибуты на self (текущий экземпляр класса), в то время как setUpClass может обращаться только к статическим атрибутам класса TestWikipedia.

Тестирование одной страницы может показаться не очень мощным или интересным, но, как вы могли помнить из предыдущих статей, построение веб-скрейперов, которые могут последовательно переходить по всем страницам сайта, относительно просто. Что произойдет, если вы объедините веб-скрейпер с модульным тестом, который делает утверждение о каждой странице?

В следующем примере мы рассмотрим, как можно тестировать веб-страницу на Wikipedia, переходя по ссылкам и проверяя свойства каждой страницы. Мы используем Python, BeautifulSoup и unittest для создания тестов, которые проверяют, соответствует ли заголовок страницы последней части URL и существует ли основной контент на странице.

Основные моменты:

  • Загрузка страницы: Для каждого теста страница загружается только один раз, что уменьшает количество необходимых запросов и повышает производительность.
  • Избегание больших объемов информации в памяти: Мы не храним все страницы в памяти, а обрабатываем их по одной, что предотвращает переполнение памяти при работе с большим количеством данных.
  • Тестирование множества страниц: Вместо тестирования одной страницы мы тестируем свойства первых 10 страниц, найденных при переходе по случайным ссылкам.

Пример кода:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import unittest
import re
import random
from urllib.parse import unquote

class TestWikipedia(unittest.TestCase):
    def test_PageProperties(self):
        self.url = 'http://en.wikipedia.org/wiki/Monty_Python'
        # Тестируем свойства первых 10 страниц, которые мы встречаем
        for i in range(1, 10):
            self.bs = BeautifulSoup(urlopen(self.url), 'html.parser')
            titles = self.titleMatchesURL()
            self.assertEqual(titles[0], titles[1])
            self.assertTrue(self.contentExists())
            self.url = self.getNextLink()
        print('Done!')

    def titleMatchesURL(self):
        # Проверяем, что текст заголовка страницы соответствует URL
        pageTitle = self.bs.find('h1').get_text()
        urlTitle = self.url[(self.url.index('/wiki/')+6):]
        urlTitle = urlTitle.replace('_', ' ')
        urlTitle = unquote(urlTitle)
        return [pageTitle.lower(), urlTitle.lower()]

    def contentExists(self):
        # Проверяем, что на странице есть div с id="mw-content-text"
        content = self.bs.find('div', {'id': 'mw-content-text'})
        if content is not None:
            return True
        return False

    def getNextLink(self):
        # Возвращает случайную ссылку на странице, используя метод из главы 3
        links = self.bs.find('div', {'id': 'bodyContent'}).find_all('a', href=re.compile('^(/wiki/)((?!:).)*$'))
        randomLink = random.SystemRandom().choice(links)
        return 'https://wikipedia.org{}'.format(randomLink.attrs['href'])

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

Как это работает:

  1. Метод test_PageProperties: Это основной тест, который выполняет проверки для первых 10 страниц, начиная со страницы «Monty Python» на Wikipedia.
  2. Метод titleMatchesURL: Проверяет, совпадает ли текст заголовка страницы с последней частью её URL. Специальные символы и пробелы обрабатываются для точного сравнения.
  3. Метод contentExists: Проверяет наличие блока контента (div с id="mw-content-text"). Это гарантирует, что страница содержит основной текстовый контент, а не является служебной или пустой.
  4. Метод getNextLink: Извлекает случайную ссылку на другую статью Wikipedia из текущей страницы для продолжения тестирования. Это позволяет тестировать не одну страницу, а цепочку страниц, переходя по ссылкам.

Когда вы пишете модульные тесты, особенно важно получать информативные сообщения об ошибках, чтобы быстро понять, что именно пошло не так. Рассмотрим два разных способа утверждения (assertion) в тестах и их влияние на процесс отладки.

Сравнение результатов: Boolean Assertion vs. assertEquals

  1. Boolean Assertion (assertTrue):Если вы используете assertTrue для проверки условия и тест не проходит, сообщение об ошибке часто не дает понимания о причине проблемы. Вам просто сообщают, что ожидалось True, а получено False.
    FAIL: test_PageProperties (
    main
    .TestWikipedia)
    Traceback (most recent call last):
    File "15-3.py", line 22, in test_PageProperties
    self.assertTrue(self.titleMatchesURL())
    AssertionError: False is not true
  2. Assertion with assertEquals:Использование assertEquals для сравнения двух значений напрямую делает ошибки более понятными. Если утверждение не выполняется, вы увидите, какие именно значения были различны, что значительно упрощает отладку.
    FAIL: test_PageProperties (
    main
    .TestWikipedia)
    Traceback (most recent call last):
    File "15-3.py", line 23, in test_PageProperties
    self.assertEquals(titles[0], titles[1])
    AssertionError: 'lockheed u-2' != 'u-2 spy plane'

Почему важно возвращать значения?

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

Рекомендация

Используйте assertEquals или аналогичные функции для сравнения конкретных значений в тестах. Это обеспечивает более информативные сообщения об ошибках и ускоряет процесс дебаггинга и разработки.

Пример кода с assertEquals

Вот как можно адаптировать код, чтобы использовать assertEquals для более информативного вывода:

from urllib.request import urlopen
from bs4 import BeautifulSoup
import unittest
import re
import random
from urllib.parse import unquote

class TestWikipedia(unittest.TestCase):
    def test_PageProperties(self):
        self.url = 'http://en.wikipedia.org/wiki/Monty_Python'
        # Тестируем свойства первых 10 страниц, которые мы встречаем
        for i in range(1, 10):
            self.bs = BeautifulSoup(urlopen(self.url), 'html.parser')
            titles = self.titleMatchesURL()
            self.assertEqual(titles[0], titles[1])
            self.assertTrue(self.contentExists())
            self.url = self.getNextLink()
        print('Done!')

    def titleMatchesURL(self):
        # Проверяем, что текст заголовка страницы соответствует URL
        pageTitle = self.bs.find('h1').get_text()
        urlTitle = self.url[(self.url.index('/wiki/')+6):]
        urlTitle = urlTitle.replace('_', ' ')
        urlTitle = unquote(urlTitle)
        return [pageTitle.lower(), urlTitle.lower()]

    def contentExists(self):
        # Проверяем, что на странице есть div с id="mw-content-text"
        content = self.bs.find('div', {'id': 'mw-content-text'})
        if content is not None:
            return True
        return False

    def getNextLink(self):
        # Возвращает случайную ссылку на странице, используя метод из главы 3
        links = self.bs.find('div', {'id': 'bodyContent'}).find_all('a', href=re.compile('^(/wiki/)((?!:).)*$'))
        randomLink = random.SystemRandom().choice(links)
        return 'https://wikipedia.org{}'.format(randomLink.attrs['href'])

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

Объяснение ключевых моментов:

  1. test_PageProperties метод:
    • Здесь мы начинаем тест с URL http://en.wikipedia.org/wiki/Monty_Python.
    • Используем цикл для тестирования первых 10 страниц, на которые мы можем перейти, начиная с данной страницы.
    • Для каждой страницы загружаем HTML-содержимое и парсим его с помощью BeautifulSoup.
    • titles = self.titleMatchesURL() вызывает метод, который проверяет соответствие заголовка страницы и URL.
    • self.assertEqual(titles[0], titles[1]) сравнивает два значения: заголовок страницы и часть URL. Если они не совпадают, тест выдаст информативное сообщение об ошибке.
    • self.assertTrue(self.contentExists()) проверяет, существует ли на странице блок контента с id="mw-content-text".
    • self.url = self.getNextLink() обновляет URL для перехода к следующей странице.
  2. titleMatchesURL метод:
    • Извлекает текст заголовка страницы и последнюю часть URL, преобразуя _ в пробелы и декодируя URL.
    • Возвращает оба значения в нижнем регистре для сравнения.
  3. contentExists метод:
    • Проверяет наличие div с id="mw-content-text" и возвращает True, если такой блок существует, иначе False.
  4. getNextLink метод:
    • Находит все внутренние ссылки Wikipedia в контенте страницы и выбирает случайную ссылку для следующего шага тестирования.

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

Тестирование с использованием Selenium

JavaScript представляет собой особые вызовы при тестировании веб-сайтов, особенно когда вы работаете с динамически генерируемым контентом, как в случае с Ajax, который был рассмотрен в статье про скрейпинг JavaScript. К счастью, Selenium предоставляет отличный фреймворк для работы с особенно сложными веб-сайтами. Библиотека изначально была разработана именно для тестирования веб-сайтов!

Синтаксис тестов на Python с использованием модуля unittest и тестов с использованием Selenium имеет заметные отличия. Selenium не требует, чтобы тесты были организованы в функции внутри классов; его утверждения (assert) не требуют скобок; и тесты проходят без вывода сообщений, генерируя сообщения только при наличии ошибок.

Пример простого теста на Selenium

Вот как может выглядеть базовый тест на Selenium для проверки заголовка веб-страницы:

from selenium import webdriver

# Инициализация драйвера, PhantomJS здесь использоваться не будет, так как он устарел.
# Вместо этого рассмотрим использование Chrome в режиме headless.
driver = webdriver.Chrome(options=webdriver.ChromeOptions().add_argument('headless'))

# Переходим на страницу
driver.get('http://en.wikipedia.org/wiki/Monty_Python')

# Проверяем, содержится ли 'Monty Python' в заголовке страницы
assert 'Monty Python' in driver.title

# Закрываем драйвер
driver.close()

Ключевые моменты кода

  1. Инициализация драйвера:
    • В примере использован драйвер Chrome. Selenium поддерживает разные браузеры, но для каждого нужен свой драйвер.
    • headless режим означает, что браузер запускается и работает без графического интерфейса, что идеально подходит для автоматических тестов.
  2. Переход на страницу:
    • Метод get используется для загрузки страницы по указанному URL.
  3. Проверка условий (assertions):
    • assert в Selenium используется для проверки условий. Если условие не выполняется, тест вызовет ошибку и остановится.
    • В данном случае проверяется, что строка ‘Monty Python’ присутствует в заголовке страницы (driver.title).
  4. Закрытие драйвера:
    • Важно закрывать драйвер после выполнения тестов, чтобы освободить ресурсы.

Более сложные проверки и возможности Selenium

Selenium позволяет выполнять гораздо более сложные проверки и манипуляции с веб-страницами:

  • Поиск элементов: Можно искать элементы на странице с помощью методов find_element_by_* и find_elements_by_*.
  • Интерактивность: Selenium может имитировать действия пользователя, такие как клики по ссылкам, ввод текста в формы и другие.
  • Ожидания (Waits): Selenium может ожидать определённые условия на странице перед продолжением теста, что полезно для работы с асинхронным JavaScript.

Взаимодействие с сайтом

Ситуация: необходимо связаться с местным малым бизнесом через форму обратной связи на их сайте, но  форма не работает: ничего не происходило при нажатии на кнопку отправки. После небольшого исследования стало заметно, что они использовали простую форму с mailto, предназначенную для отправки электронного письма с содержимым формы. К счастью, можно использовать эту информацию, чтобы отправить им письмо, объяснить проблему с их формой.

Если бы мы написали традиционный скрейпер, который использовал бы или тестировал эту форму, мой скрейпер, скорее всего, просто скопировал бы структуру формы и отправил бы письмо напрямую — обойдя саму форму. Как мне проверить функциональность формы и убедиться, что она работает идеально через браузер?

Хотя в предыдущих статьях обсуждались навигация по ссылкам, отправка форм и другие типы взаимодействий, в основе всего, что мы делали, лежит стремление обойти интерфейс браузера, а не использовать его. В отличие от этого, Selenium может вводить текст, нажимать кнопки и делать все операции через браузер (в данном случае, через браузер без интерфейса PhantomJS), а также обнаруживать такие проблемы, как неработающие формы, плохо написанный JavaScript, опечатки в HTML и другие проблемы, которые могут стать препятствием для реальных пользователей.

Ключевым элементом такого тестирования является концепция элементов Selenium. Этот объект был кратко рассмотрен статье про скрейпинг в JavaScript  и возвращается при вызове, например:

usernameField = driver.find_element_by_name('username')

Так же, как существует множество действий, которые вы можете выполнить с различными элементами веб-сайта в вашем браузере, Selenium может выполнить множество действий с любым элементом.

Среди них:

myElement.click()  # Нажать на элемент
myElement.click_and_hold()  # Нажать и удерживать элемент
myElement.release()  # Отпустить нажатие
myElement.double_click()  # Двойной клик по элементу
myElement.send_keys_to_element('content to enter')  # Ввести текст в элемент

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

Различия в методах заполнения форм

Чтобы увидеть разницу между методами работы с веб-формами, рассмотрим страницу с формой по адресу http://pythonscraping.com/pages/files/form.html, которая уже использовалась в качестве примера в статье про авторизацию и формы. Мы можем заполнить эту форму и отправить её следующими способами:

from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains

# Инициализируем драйвер PhantomJS
driver = webdriver.PhantomJS(executable_path='<Путь к Phantom JS>')
driver.get('http://pythonscraping.com/pages/files/form.html')

# Находим поля ввода по их именам
firstnameField = driver.find_element_by_name('firstname')
lastnameField = driver.find_element_by_name('lastname')

# Находим кнопку отправки по её идентификатору
submitButton = driver.find_element_by_id('submit')

### МЕТОД 1 ###
firstnameField.send_keys('Ryan')  # Вводим имя
lastnameField.send_keys('Mitchell')  # Вводим фамилию
submitButton.click()  # Нажимаем кнопку отправки
################

### МЕТОД 2 ###
actions = ActionChains(driver) \
    .click(firstnameField).send_keys('Ryan') \
    .click(lastnameField).send_keys('Mitchell') \
    .send_keys(Keys.RETURN)  # Используем клавишу RETURN для отправки формы

actions.perform()  # Выполняем цепочку действий
################

# Выводим результат работы формы
print(driver.find_element_by_tag_name('body').text)

# Закрываем браузер
driver.close()

Метод 1 вызывает метод send_keys для каждого поля и затем кликает по кнопке отправки.

Метод 2 использует единую цепочку действий (ActionChains), чтобы кликнуть и ввести текст в каждое поле, что происходит последовательно после вызова метода perform. Этот скрипт работает одинаково, независимо от того, используется первый метод или второй, и выводит строку:

Hello there, Ryan Mitchell!

Есть и другие отличия между двумя методами, помимо объектов, которые они используют для выполнения команд: обратите внимание, что первый метод кликает по кнопке «Submit», в то время как второй использует клавишу Return для отправки формы, пока текстовое поле активно.

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

Перетаскивание элементов

Кликать по кнопкам и вводить текст — это одно, но особенно хорошо Selenium справляется с относительно новыми формами веб-взаимодействия, такими как интерфейсы перетаскивания (drag-and-drop). Selenium позволяет легко управлять такими интерфейсами. Для использования функции перетаскивания вам нужно указать исходный элемент (элемент, который будет перетаскиваться) и либо смещение, на которое его нужно перетащить, либо целевой элемент, куда его следует перетащить.

На демонстрационной странице по адресу http://pythonscraping.com/pages/javascript/draggableDemo.html представлен пример такого интерфейса:

from selenium import webdriver
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver import ActionChains

# Инициализация драйвера PhantomJS
driver = webdriver.PhantomJS(executable_path='<Путь к Phantom JS>')
driver.get('http://pythonscraping.com/pages/javascript/draggableDemo.html')

# Выводим начальное сообщение из элемента с id 'message'
print(driver.find_element_by_id('message').text)

# Находим элемент, который будем перетаскивать, и целевой элемент
element = driver.find_element_by_id('draggable')
target = driver.find_element_by_id('div2')

# Создаем цепочку действий для перетаскивания
actions = ActionChains(driver)
actions.drag_and_drop(element, target).perform()

# Выводим сообщение после перетаскивания
print(driver.find_element_by_id('message').text)

# Закрываем драйвер
driver.close()

Два сообщения выводятся из блока с сообщениями на демо-странице. Первое гласит:

Докажите, что вы не робот, перетащив квадрат из синей области в красную!

Затем, сразу после выполнения задачи, содержимое выводится снова, и теперь оно гласит:

Вы определенно не робот!

Как предполагает демонстрационная страница, перетаскивание элементов для доказательства, что вы не робот, является общей темой во многих CAPTCHA. Хотя боты могут перетаскивать объекты уже долгое время (это всего лишь клик, удержание и перемещение), почему-то идея использовать «перетащите это» в качестве проверки человечности все еще жива.

Кроме того, библиотеки CAPTCHA с перетаскиванием редко используют задачи, сложные для ботов, например, «перетащите изображение котенка на изображение коровы» (что требует от вас идентификации изображений как «котенок» и «корова», а также анализа инструкций). Вместо этого они часто включают упорядочивание чисел или какую-то другую довольно тривиальную задачу, подобную приведенному выше примеру.

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

Этот пример демонстрирует, как можно автоматизировать взаимодействие с более сложными элементами веб-интерфейса с помощью Selenium, имитируя действия пользователя, включая сложные взаимодействия, такие как перетаскивание.

Создание скриншотов

Помимо обычных возможностей тестирования, у Selenium есть интересный трюк, который может упростить ваше тестирование (или произвести впечатление на вашего начальника): скриншоты. Да, фотографические доказательства можно создать из юнит-тестов, без необходимости нажимать клавишу PrtScn:

from selenium import webdriver

# Инициализация драйвера PhantomJS
driver = webdriver.PhantomJS()
driver.get('http://www.pythonscraping.com/')

# Сохраняем скриншот в файл в локальной папке tmp
driver.get_screenshot_as_file('tmp/pythonscraping.png')

# Закрываем драйвер
driver.quit()

Этот скрипт переходит на сайт http://pythonscraping.com и затем сохраняет скриншот главной страницы в локальной папке tmp (папка должна существовать, чтобы скриншот сохранялся корректно). Скриншоты могут быть сохранены в различных форматах изображений.

Как это использовать на практике

  1. Подготовка папки для скриншотов: Убедитесь, что папка, в которую вы хотите сохранить скриншот, существует. Например, если вы хотите сохранить скриншот в папку tmp в корне вашего проекта, проверьте, что она создана. Если нет, создайте её:
    import os
    
    # Проверяем наличие папки tmp и создаем, если ее нет
    if not os.path.exists('tmp'):
        os.makedirs('tmp')
  2. Использование других форматов изображений: Вы можете сохранять скриншоты в различных форматах, изменяя расширение файла в методе get_screenshot_as_file. Например, для сохранения в формате JPEG, используйте:
    driver.get_screenshot_as_file('tmp/pythonscraping.jpg')
  3. Скриншоты различных элементов: Если вам нужно сделать скриншот не всей страницы, а определённого элемента, Selenium позволяет это сделать через элементы и PIL (Python Imaging Library):
    from PIL import Image
    from io import BytesIO
    
    # Находим элемент, скриншот которого нужно сделать
    element = driver.find_element_by_id('some-element-id')
    
    # Создаем скриншот всей страницы и открываем его через PIL
    png = driver.get_screenshot_as_png()  # Получаем скриншот всей страницы в виде PNG
    im = Image.open(BytesIO(png))
    
    # Используем размеры элемента для обрезки изображения
    left = element.location['x']
    top = element.location['y']
    right = left + element.size['width']
    bottom = top + element.size['height']
    
    im = im.crop((left, top, right, bottom))  # Обрезаем изображение
    im.save('tmp/element_screenshot.png')  # Сохраняем обрезанное изображение
  4. Применение в автоматическом тестировании: Скриншоты часто используются в автоматических тестах для документирования ошибок или подтверждения визуального соответствия ожиданиям. В таких случаях после обнаружения ошибки можно автоматически сохранять скриншот:
    try:
        # Проверяем наличие какого-то важного элемента
        important_element = driver.find_element_by_id('important-element')
    except Exception as e:
        driver.get_screenshot_as_file(f'errors/{driver.title}_error.png')
        raise AssertionError("Важный элемент не найден!") from e

Использование unittest и Selenium вместе

Синтаксическая строгость и подробность модуля unittest Python могут быть желательны для больших наборов тестов, в то время как гибкость и мощь теста Selenium могут быть единственным вариантом для тестирования некоторых функций веб-сайта. Так что же выбрать?

Вот секрет: вам не нужно выбирать одно. Selenium может быть легко использован для получения информации о веб-сайте, а unittest может оценить, соответствует ли эта информация критериям для прохождения теста. Нет никаких препятствий для импорта инструментов Selenium в unittest Python, объединяя лучшее из обоих миров.

Например, следующий скрипт создает модульный тест для интерфейса перетаскивания на веб-сайте, утверждая, что он корректно сообщает «You are definitely not a bot!» после того, как один элемент был перетащен в другой:

from selenium import webdriver
from selenium.webdriver import ActionChains
import unittest

class TestDragAndDrop(unittest.TestCase):
    driver = None

    def setUp(self):
        # Инициализация драйвера PhantomJS
        self.driver = webdriver.PhantomJS(executable_path='<Путь к PhantomJS>')
        url = 'http://pythonscraping.com/pages/javascript/draggableDemo.html'
        self.driver.get(url)

    def tearDown(self):
        # Закрываем браузер после каждого теста
        self.driver.quit()
        print("Tearing down the test")

    def test_drag(self):
        # Перетаскиваем элемент и проверяем результат
        element = self.driver.find_element_by_id('draggable')
        target = self.driver.find_element_by_id('div2')
        actions = ActionChains(self.driver)
        actions.drag_and_drop(element, target).perform()
        
        # Проверяем, что текст сообщения соответствует ожидаемому
        self.assertEqual('You are definitely not a bot!',
                         self.driver.find_element_by_id('message').text)

if __name__ == '__main__':
    unittest.main(argv=[''], exit=False)

Почти любой аспект веб-сайта может быть протестирован с помощью комбинации unittest и Selenium Python. Фактически, в сочетании с некоторыми библиотеками обработки изображений из статьи «Парсинг изображений», вы даже можете сделать скриншот веб-сайта и тестировать его на уровне каждого пикселя!

Как это использовать на практике

  1. Интеграция с CI/CD: Такие тесты можно интегрировать в системы непрерывной интеграции и доставки (CI/CD), чтобы автоматически проверять функциональность веб-интерфейсов при каждом коммите.
  2. Расширенное тестирование: Вы можете расширять тесты, добавляя проверки различных состояний веб-приложения, например, проверяя состояния после асинхронных операций JavaScript.
  3. Тестирование на разных устройствах: Используя разные драйверы (например, ChromeDriver, FirefoxDriver и т.д.), можно тестировать веб-приложение в разных браузерах и даже симулировать разные устройства.
  4. Визуальное регрессионное тестирование: Сохраняя скриншоты ключевых страниц, можно использовать их для визуального регрессионного тестирования, сравнивая текущий вид страниц с эталонными скриншотами.
  5. Обработка ошибок: При обнаружении ошибок в тестах можно сохранять скриншоты текущего состояния страницы, что поможет в диагностике проблем.

Индивидуальное и групповое обучение «Аналитик данных»
Если вы хотите стать экспертом в аналитике, могу помочь. Запишитесь на мой курс «Аналитик данных» и начните свой путь в мир ИТ уже сегодня!

Контакты
Для получения дополнительной информации и записи на курсы свяжитесь со мной:

Телеграм: https://t.me/Vvkomlev
Email: victor.komlev@mail.ru

Объясняю сложное простыми словами. Даже если вы никогда не работали с ИТ и далеки от программирования, теперь у вас точно все получится! Проверено десятками примеров моих учеников.

Гибкий график обучения. Я предлагаю занятия в мини-группах и индивидуально, что позволяет каждому заниматься в удобном темпе. Вы можете совмещать обучение с работой или учебой.

Практическая направленность. 80%: практики, 20% теории. У меня множество авторских заданий, которые фокусируются на практике. Вы не просто изучаете теорию, а сразу применяете знания в реальных проектах и задачах.

Разнообразие учебных материалов: Теория представлена в виде текстовых уроков с примерами и видео, что делает обучение максимально эффективным и удобным.

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

Понравилась статья? Поделиться с друзьями:
Школа Виктора Комлева
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!:

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