Django TODO: тестирование во время конструирования

Fri 29 June 2012

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

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

Цель тестирования противоположна целям других этапов разработки. Его целью является нахождение ошибок. Успешным считается тест, нарушающий работу ПО. Все остальные этапы разработки направлены на предотвращение ошибок и недопущение нарушения работы программы [1].

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

Виды тестирования, выполняемые разработчиком: блочное тестирование, интеграционное тестирование.

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

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

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

Тестирование требует, чтобы программист рассчитывал найти ошибки в своем коде [1].

Рекомендации по тестированию Django приложений

Карл Майер дал следующие рекомендации по тестированию Django приложений [2]:

  • для представлений следует писать интеграционные тесты;
  • для тестирования Ajax и других JavaScript взаимодействий нужно использовать Selenium, который позволяет автоматизировать тестирование веб-приложений в браузере;
  • для всех остальных случаев необходимо использовать блочное тестирование.

Он также выступил против использования fixtures (набор данных, которые Django использует для импорта в БД), аргументируя тем, что они медленно загружаются, их трудно поддерживать и обновлять. Также применение fixtures приводит к росту взаимозависимости тестов. Для замены fixtures был разработан инструмент factory_boy, который имеет следующие преимущества: близость тестовых данных к тестовому коду, не требует от тестирования введение избыточных данных, прост в обслуживании.

Что именно тестировать?

Ответ на вопрос "Что именно тестировать в Django приложении?" дал Дэниэл Линдсли [3]:

  • не следует тестировать стандартные функции Python и Django;
  • если модель данных содержит пользовательские методы, их необходимо тестировать;
  • нужно тестировать пользовательские формы, шаблонные теги, контекстные процессоры, команды управления;
  • следует тестировать бизнес логику в представлениях.

Тестирование моделей данных

Для тестирования Django моделей используют блочное тестирование. В качестве инструмента блочного тестирования автор выбрал библиотеку unittest, так как она входит в стандартную библиотеку Python. Тесты, написанные на unittest работают быстрее при тестировании Django приложений [3].

В качестве примера проектирования тестов приводятся методы days_quantity_after_deadline (определяет количество просроченных дней задачи) и start_date (определяет дату начала работы над задачей).

Количество просроченных дней задачи

Определение количества просроченных дней задачи. Для этого рассмотрим все возможные случаи наступления дедлайна:

  • просрочен дедлайн у ожидающей задачи из-за предыдущей задачи. Предыдущая задача превысила свой дедлайн и дедлайн текущей задачи;
  • работающая задача превысила дедлайн;
  • задача выполнена с превышением дедлайна;
  • просрочен дедлайн у остановленной задачи. Владелец цепочки не решил проблему остановки задачи.

Из вышеперечисленных случаев следует два правила расчета количества просроченных дней:

  • если статус DONE и дата окончания задачи больше или равна дедлайну, то количество дней равно разности даты окончания и дедлайна, плюс один день (день дедлайна);
  • если статус WAIT/WORK/STOP и текущая дата больше или равна дедлайну, то количество дней равно разности текущей даты и дедлайна, плюс один день (день дедлайна).

Определив все возможные случаи наступления дедлайна можно спроектировать "чистые тесты" для метода days_quantity_after_deadline:

class DeadlineDaysTest(TaskTest):
    """Тестирует определение количества просроченных дней."""

    def task_wait_overdue(self):
        """Просрочен дедлайн у ожидающей задачи из-за предыдущей задачи.

        Предыдущая задача превысила свой дедлайн и дедлайн текущей задачи.
        """

    def task_work_overdue(self):
        """Работающая задача превысила дедлайн."""

    def task_done_overdue(self):
        """Задача выполнена с превышением дедлайна."""

    def task_stop_overdue(self):
        """Просрочен дедлайн у остановленной задачи.

        Владелец цепочки не решил проблему остановки задачи.
        """

Под "чистыми тестами" подразумеваются тесты, которые проверяют работает ли код, а не пытаются нарушить его работу всевозможными способами ("грязные тесты"). В организациях со зрелым процессом тестирования на каждый "чистый тест" обычно приходятся пять "грязных" [1].

Дата начала работы над задачей

Рассмотрим проектирование тестов для более сложного метода -- метода, который определяет дату начала работы над задачей.

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

Статус текущей задачи WAIT

Задача стоит первой в цепочке, дата начала работы над цепочкой не наступила. В этом случае дата начала работы над задачей равна дате начала цепочки.

Предыдущая задача имеет статус WAIT, к тому же:

  • дата начала предыдущей задачи не наступила. В данном случае дата начала текущей задачи равна дедлайну предыдущей задачи;
  • наступила дата начала предыдущей задачи, но еще не наступил дедлайн предыдущей задачи. В данном случае дата начала текущей задачи равна дедлайну предыдущей задачи;
  • наступил дедлайн предыдущей задачи. В данном случае дата начала текущей задачи не прогнозируема.

Предыдущая задача имеет статус WORK или STOP, к тому же:

  • не наступил дедлайн предыдущей задачи. В данном случае дата начала текущей задачи равна дедлайну предыдущей задачи;
  • наступил дедлайн предыдущей задачи. В данном случае дата начала текущей задачи не прогнозируема.

Статус текущей задачи WORK или DONE или STOP

Задача стоит первой в цепочке, наступила дата начала работы над цепочкой. В данном случае дата начала работы над задачей равна дате начала цепочки.

Предыдущая задача имеет статус DONE. В данном случае дата начала задачи равна дедлайну предыдущей задачи.

Правило определения даты

Проанализировав вышеперечисленные случаи автор сформулировал правила определения даты начала работы над задачей:

  • для первой задачи равна дате начала работы над цепочкой;
  • для статуса WAIT равна дедлайну предыдущей задачи. Если дедлайн просрочен, дата начала задачи не прогнозируема;
  • для статусов WORK, DONE, STOP равна дате окончания предыдущей задачи.

Демонстрация фрагмента блочного тестирования для метода start_date приведена ниже:

# -*- coding: utf-8 -*-
import datetime

from django.test import TestCase
from django.contrib.auth.models import User

from todo.models import Chain, Task
from . import factories


class TaskTest(TestCase):

    def setUp(self):
        factories.make_fixtures()
        # Сотрудники.
        self.manager = User.objects.get(username='alexander')
        self.designer = User.objects.get(username='kazimir')
        self.programmer = User.objects.get(username='ada')


class StartDateTest(TaskTest):
    """Тестирует определение даты начала работы над задачей."""

    def test_first_task(self):
        """Тестирует дату начала работы первой задачи.

        Дата начала первой задачи совпадает с датой начала цепочки. Это условие
        верно для задач с любым статусом.
        """
        today = datetime.date.today()
        chain_start_date = today + datetime.timedelta(days=1)
        chain = Chain.objects.create(name='Chain', start_date=chain_start_date,
                                     owner=self.manager)
        deadline = chain_start_date + datetime.timedelta(days=3)
        first_task = Task.objects.create(worker=self.designer, task='Design',
                                         deadline=deadline, chain=chain)
        self.assertEqual(first_task.start_date(), chain.start_date)

Заключение

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

Тестирование представлений

Для тестирования Django представлений рекомендуется [4] использовать библиотеку WebTest. Ближайшим аналогом WebTest является twill, но он не поддерживает юникод и давно не развивается (последний релиз был в 2007 году).

В качестве примера приводится тестирование посещения пользователем страницы актуальных задач:

# -*- coding: utf-8 -*-
from django_webtest import WebTest

from django.core.urlresolvers import reverse

from . import factories


class ActualTasksTest(WebTest):

    def setUp(self):
        factories.make_fixtures()

    def test_user_not_logined(self):
        response = self.app.get(reverse('todo_actual_tasks'))
        self.assertEqual(response.status_int, 302)

    def test_designer_logined(self):
        response = self.app.get(reverse('todo_actual_tasks'), user='kazimir')
        assert 'Казимир Малевич' in response

В первом случае пользователь не авторизован (метод test_user_not_logined) и браузер должен вернуть статус 302 (перенаправление на страницу авторизации), во втором случае (метод test_designer_logined) пользователь авторизован под именем Казимир Малевич. Данные тесты не такие полезные, как блочные тесты. Но даже если они просто проверят основные страницы системы на отсутствие сообщений об исключении, то они уже принесут большую пользу разработчику.

Представления имеют много связей и зависимостей (шаблоны, база данных, конфигурация URL), поэтому их трудно тестировать. Карл Майер рекомендует писать как можно меньше кода на уровне представлений [2].

[1](1, 2, 3) Макконелл С. Совершенный код. Мастер-класс / Пер. с англ. – М. : Издательство "Русская редакция", 2012. – 896 стр. : ил.
[2](1, 2) Meyer C. Testing and Django at PyCon US 2012.
[3](1, 2) Линдсли Д. Guide to Testing in Django.
[4]Коробов М. Пишем функциональные/интеграционные тесты для проекта на Django.

Category: Python Tagged: python django django-todo testing

Comments