Тестирование приложения python, CI/CD

5 (100%) 1 vote[s]



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

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

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

import math
 
def is_prime(n):
    """Determines if a non-negative integer is prime."""
    if n < 2:
        return False
    for i in range(2, int(math.sqrt(n)):
        if n % i == 0:
            return False
    return True

Эта функция проверяет, является ли целое число простым, проверяя, что оно больше 2 (наименьшее простое число) и не делится без остатка на любое число, меньшее его квадратного корня.

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

from prime import is_prime
 
def test_prime(n, expected):
  if is_prime(n) != expected:
      print(f"ERROR on is_prime({n}), expected {expected}")

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

from prime import is_prime
 
def test_prime(n, expected):
    if is_prime(n) != expected:
        print(f"ERROR on is_prime({n}), expected {expected}")
 
if __name__ == "__main__":
    test_prime(-4, False)
    test_prime(-3, False)
    test_prime(-2, False)
    test_prime(-1, False)
    test_prime(0, False)
    test_prime(1, False)
    test_prime(2, True)
    test_prime(3, True)
    test_prime(4, False)

Это можно запустить в терминале с помощью python tests0.py.

Теперь, когда тестирование автоматизировано, когда вносятся изменения prime.py, легко увидеть, решена ли проблема:

for i in range(2, int(math.sqrt(n) + 1):
    if n % i == 0:
        return False

Это была простая ошибка, не превышающая 1. range возвращает значения, начиная с первого аргумента и до второго, но не включая его.

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

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

import math
 
def is_prime(n):
  return n > 1 and all(n % i for i in range(2, int(math.sqrt(n)) + 1))

При этом проверяются два условия:

  1. Есть n больше чем 1; а также
  2. Верно ли, что все значения от 2 до квадратного корня (включительно) имеют «истинное» значение True? (Если это так, это означает, что n% i ненулевое для всех из них; т.е. n не делится без остатка ни на один из них.)

Если и только если оба из них верны, число считается простым!

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

  • Полезной функцией Python для тестирования является встроенная команда assert, за которой следует логическое выражение. Если он не оценивается True, Python выдаст AssertionError.
  • Все программы при выходе возвращают код выхода. Вообще говоря, код выхода 0 указывает, что все прошло хорошо, а любой другой код указывает на ошибку. Чтобы проверить код выхода в bash после запуска скрипта Python, используйте echo $?.

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

unittest — это встроенная библиотека Python для тестирования. Тестирование is_prime с unittest может выглядеть следующим образом :

import unittest
 
  from prime import is_prime
 
 
  class Tests(unittest.TestCase):
 
      def test_1(self):
          """Check that 1 is not prime."""
          self.assertFalse(is_prime(1))
 
      def test_2(self):
          """Check that 2 is prime."""
          self.assertTrue(is_prime(2))
 
      def test_8(self):
          """Check that 8 is not prime."""
          self.assertFalse(is_prime(8))
 
      def test_11(self):
          """Check that 11 is prime."""
          self.assertTrue(is_prime(11))
 
      def test_25(self):
          """Check that 25 is not prime."""
          self.assertFalse(is_prime(25))
 
      def test_28(self):
          """Check that 28 is not prime."""
          self.assertFalse(is_prime(28))
 
 
  if __name__ == "__main__":
      unittest.main()
  • Tests наследуется от unittest.TestCase, что означает, что он содержит серию тестов, каждый из которых может расширять базовые функции, определенные в unittest.TestCase.
  • Каждый тест внутри Tests — это просто метод с соответствующей пометкой «docstring».
  • unittest имеет ряд встроенных, более продвинутых и удобочитаемых утверждений. Вместо использования assert isPrime(1) == False просто используйте self.assertFalse(is_prime(1)).
  • unittest.main() проведет все тесты.

unittest методы включают (но не ограничиваются ими):

  • assertEqual
  • assertNotEqual
  • assertTrue
  • assertFalse
  • assertIn : проверяет, есть ли элемент в списке
  • assertNotIn : проверяет, нет ли элемента в списке

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

Бэкэнд

У Django есть собственная среда тестирования, чтобы упростить тестирование веб-приложений. Код теста находится в каталоге приложения tests.py.

Вот функция, которая может быть в Flight модели и которую, возможно, потребуется протестировать, если она будет использоваться в представлении:

def is_valid_flight(self):
      return (self.origin != self.destination) and (self.duration >= 0)
  • Это возвращается True, если исходная точка и пункт назначения не совпадают и продолжительность положительна.

Вот пример flights/tests.py:

from django.test import TestCase
 
  from .models import Airport, Flight
 
  # Создайте свои тесты здесь.
  class ModelsTestCase(TestCase):
 
      def setUp(self):
 
          # Создавайте аэропорты.
          a1 = Airport.objects.create(code="AAA", city="City A")
          a2 = Airport.objects.create(code="BBB", city="City B")
 
          # Создавайте рейсы.
          Flight.objects.create(origin=a1, destination=a2, duration=100)
          Flight.objects.create(origin=a1, destination=a1, duration=200)
          Flight.objects.create(origin=a1, destination=a2, duration=-100)
 
      def test_departures_count(self):
          a = Airport.objects.get(code="AAA")
          self.assertEqual(a.departures.count(), 3)
 
      def test_arrivals_count(self):
          a = Airport.objects.get(code="AAA")
          self.assertEqual(a.arrivals.count(), 1)
 
      def test_valid_flight(self):
          a1 = Airport.objects.get(code="AAA")
          a2 = Airport.objects.get(code="BBB")
          f = Flight.objects.get(origin=a1, destination=a2, duration=100)
          self.assertTrue(f.is_valid_flight())
 
      def test_invalid_flight_destination(self):
          a1 = Airport.objects.get(code="AAA")
          f = Flight.objects.get(origin=a1, destination=a1)
          self.assertFalse(f.is_valid_flight())
 
      def test_invalid_flight_duration(self):
          a1 = Airport.objects.get(code="AAA")
          a2 = Airport.objects.get(code="BBB")
          f = Flight.objects.get(origin=a1, destination=a2, duration=-100)
          self.assertFalse(f.is_valid_flight())
  • TestCase— это расширение фреймворка unittest, которое упрощает тестирование некоторых вещей, специфичных для приложения Django.
  • ModelsTestCase — это класс, который, как и раньше, содержит функции для каждого теста.
  • В фреймворке TestCase setUp функция будет запускаться перед любыми тестами. В этом случае для тестов создаются некоторые аэропорты и рейсы.
    • setUp фактически запускается перед каждым тестом, чтобы гарантировать, что испытания независимы.

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

Чтобы запустить тесты, просто запустите python manage.py test.

Защита от неверных данных

Мы можем написать методы в наших моделях для предотвращения нелогичных или «плохих» данных. Одним из примеров, например, может быть предотвращение попытки контент-менеджеров создавать рейсы с одним и тем же отправлением и пунктом назначения или с неположительной продолжительностью. Для этого мы можем переопределить функциональность некоторых дополнительных встроенных функций тестирования Django.

В airline1/models.py:

# Добавьте метод, который вызывает «Ошибки проверки», если данные нелогичны.
def clean(self):
    if self.origin == self.destination:
        raise ValidationError("Origin and destination must be different.")
    elif self.duration < 1:
        raise ValidationError("Duration must be positive.")
 
# Вызовите этот метод перед попыткой добавления данных, переопределив поведение по умолчанию встроенного `save`.
def save(self, *args, **kwargs):
    self.clean()
 
    # Этот синтаксис теперь вызывает собственную функцию Django "save", добавляя эти данные в базу данных (если `clean` был в порядке).
    super().save(*args, **kwargs)

Фронтенд

Теперь, когда модели протестированы, следующим шагом будет проверка представлений:

  from django.db.models import Max
  from django.test import Client, TestCase
 
  from .models import Airport, Flight, Passenger
 
  # Создайте свои тесты здесь.
  class FlightsTestCase(TestCase):
 
      #... та же установка и тестирование модели, что и раньше ...
 
      def test_index(self):
          c = Client()
          response = c.get("/")
          self.assertEqual(response.status_code, 200)
          self.assertEqual(response.context["flights"].count(), 2)
 
      def test_valid_flight_page(self):
          a1 = Airport.objects.get(code="AAA")
          f = Flight.objects.get(origin=a1, destination=a1)
 
          c = Client()
          response = c.get(f"/{f.id}")
          self.assertEqual(response.status_code, 200)
 
      def test_invalid_flight_page(self):
          max_id = Flight.objects.all().aggregate(Max("id"))["id__max"]
 
          c = Client()
          response = c.get(f"/{max_id + 1}")
          self.assertEqual(response.status_code, 404)
 
      def test_flight_page_passengers(self):
          f = Flight.objects.get(pk=1)
          p = Passenger.objects.create(first="Alice", last="Adams")
          f.passengers.add(p)
 
          c = Client()
          response = c.get(f"/{f.id}")
          self.assertEqual(response.status_code, 200)
          self.assertEqual(response.context["passengers"].count(), 1)
 
      def test_flight_page_non_passengers(self):
          f = Flight.objects.get(pk=1)
          p = Passenger.objects.create(first="Alice", last="Adams")
 
          c = Client()
          response = c.get(f"/{f.id}")
          self.assertEqual(response.status_code, 200)
          self.assertEqual(response.context["non_passengers"].count(), 1)
    • Client имитирует веб-клиента, который в целях тестирования может отправлять запросы и получать ответы от веб-сервера. Используя Client, можно моделировать запросы к разным страницам, чтобы гарантировать возврат ожидаемой информации.

    • c.get («/») просто использует объект Client для выполнения запроса GET к маршруту и возвращает ответ (сохраненный как response). Этот ответ можно проверить, проверив response.status_code и содержимое response.contexts.

    • Аргумент может быть передан URL-адресу с использованием того же синтаксиса обозначения фигурных скобок / точек, что и раньше.

    •  Flight.objects.all (). Aggregate (Max («id»)) [«id__max«] возвращает максимальное значение ID любого рейса. Это необходимо для проверки ответа на недопустимый идентификатор рейса в URL-адресе.


Selenium

Для тестирования поведения браузера, включая код JavaScript, необходим отдельный инструмент тестирования браузера. Одним из таких инструментов является Selenium, который использует веб-драйвер, позволяющий коду Python программно выдавать себя за пользователя, взаимодействующего с веб-страницей. Вот пример веб-страницы для тестирования, на которой есть счетчик, который можно увеличивать или уменьшать с помощью двух кнопок:

<html>
    <head>
        <title>Counter</title>
        <script>
 
            document.addEventListener('DOMContentLoaded', () =>  {
 
                let counter = 0;
 
                document.querySelector('#increase').onclick = () => {
                    counter++;
                    document.querySelector('h1').innerHTML = counter;
                };
 
                document.querySelector('#decrease').onclick = () => {
                    counter--;
                    document.querySelector('h1').innerHTML = counter;
                };
            });
        </script>
    </head>
    <body>
        <h2>0</h2>
        <button id="increase">+</button>
        <button id="decrease">-</button>
    </body>
</html>

Вот код Selenium Python для тестирования страницы:

import os
import pathlib
import unittest
 
from selenium import webdriver
 
# Удобная функция для преобразования имени файла в полный путь, как это необходимо для браузера.
def file_uri(filename):
    return pathlib.Path(os.path.abspath(filename)).as_uri()
 
driver = webdriver.Chrome()
 
class WebpageTests(unittest.TestCase):
 
    def test_title(self):
        driver.get(file_uri("counter.html"))
        self.assertEqual(driver.title, "Counter")
 
    def test_increase(self):
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "1")
 
    def test_decrease(self):
        driver.get(file_uri("counter.html"))
        decrease = driver.find_element_by_id("decrease")
        decrease.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "-1")
 
    def test_multiple_increase(self):
        driver.get(file_uri("counter.html"))
        increase = driver.find_element_by_id("increase")
        for i in range(3):
            increase.click()
        self.assertEqual(driver.find_element_by_tag_name("h1").text, "3")
 
 
if __name__ == "__main__":
    unittest.main()
  • file_uri принимает HTML-файл и возвращает URL-адрес, по которому будет осуществляться доступ к этому файлу.
  • webdriver.Chrome() является одним из многих встроенных веб-драйверов Selenium. Этот, в частности, предназначен для взаимодействия с Google Chrome.
  • driver.get откроет любой переданный URL.
  • Каждая кнопка тестируется программно, путем поиска каждого элемента button по идентификатору и вызова функции click для имитации нажатия на нее пользователя. Затем можно проверить соответствие отображаемого текста ожидаемому значению.

Непрерывная интеграция и непрерывная доставка

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

Существует множество различных инструментов, предназначенных для облегчения CI и тестирования. Один из самых популярных и используемый в этом классе — Travis. Когда код помещается на GitHub, GitHub уведомляет Travis об этих изменениях. Трэвис извлечет этот код и проведет на нем несколько тестов. После этого GitHub получит уведомление о результатах теста.

Файл конфигурации Трэвиса, в котором перечислены все тесты, установки и т. д., написан в формате YAML. Файлы YAML состоят из ключей и значений, как и JSON:

key1: value1
key2: value2
key3:
    * item1
    * item2
    * item3
key4:
    nested_key1: value3
    nested_key2:
        * item4
        * item5

В частности, очень простой файл Travis YML будет выглядеть примерно так:

language: python
python: 3.6
install: pip install -r requirements.txt
script: python manage.py test
  • install перечисляет команды, которые следует выполнить для установки всех необходимых компонентов перед тестированием. Перечисление любых требований, таких как Django, в файле requirements.txt автоматизирует эту установку.
  • script перечисляет команду для фактического запуска тестов.
  • Чтобы настроить Travis, перейдите на https://travis-ci.org и синхронизируйте учетную запись GitHub. Затем можно выбрать любые репозитории, которые должен отслеживать Трэвис. После отправки на GitHub он будет виден на веб-сайте Travis как «сборка» и будет выполнять команды, как указано в файле конфигурации. Трэвис может проверить, прошли ли тесты, проверив код выхода команды тестирования. Если сборка не проходит какие-либо тесты, это будет отмечено в журнале фиксации GitHub красным крестиком. Тестируемая сборка будет отмечена желтой точкой, а успешная сборка будет отмечена зеленой галочкой.

Предыдущая статья «Django«

Читайте больше по теме:

Подписаться
Уведомление о
guest
0 комментариев
Inline Feedbacks
View all comments
Просмотры: 150

Популярные записи