Найти - Пользователи
Полная версия: TestCase и time.sleep
Начало » Python для экспертов » TestCase и time.sleep
1 2 3 4
Андрей Светлов
А я вам о чем?
Kogrom
У меня вопрос по теме. Корректно ли править сам класс, чтобы он не зависел от времени? Как-то так:
class Billing:
def can_show(self, date=None):
if not date:
date = datetime.now()
...
Вроде бы класс после этого только универсальнее становится. Или нет?
Андрей Светлов
Вы задали неодноздачный вопрос.
Исходите из того, какая сигнатура требуется системе “для обычной работы”.

Если ваш Billing предполагает, что в нормальном режиме (рабочем коде его использования) can_show иногда будет вызываться с указанием даты - отлично!
Так и поступайте!

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

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

И не следует указывать в параметрах по умолчанию те, которые нужны только для юниттестов.

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

Мне случалось работать над проектом, в котором приличная часть параметров “по умолчанию” нужна была только для тестов.
Это очень неуютно.

Если параметр есть - значит, его кто-то использует.
Начинаю спрашивать у программистов и аналитиков: а какой-такой бизнес-процесс требует указания даты в can_show? Никто не может ответить.

Паника усиливается: есть часть кода, о которой никто не может сказать - зачем она?
Поиском нахожу все вызовы метода в рабочем коде. Они этот параметр не используют. Тесты - да.
Тесты вообще с кодом творят страшные вещи - им бы только отработать нормально :) А тестов у нас много, тысяч надцать.
Юниттесты еще белые и пушистые, всё на виду. А функциональные да регрессионные - полный ахтунг, черт ногу сломит. А то и башку расшибет.
Может, я плохо искал? Питон ведь очень динамический язык!

В отчаянии обращаюсь к начальнику отдела. Он морщит лоб и устраивает короткое совещание еще с тремя начальниками.
Эти достойные люди не могут понять, в чем дело.
Затем одного осеняет - да! Это мы сделали так, чтобы тесты писать было легче! А потом напрочь забыли.

Решение вылилось в довольно много человекочасов (моих и сотрудников).
А всё потому, что при разработке думали об удобстве автоматического тестирования, забыв главное правило - программа создается для конечного пользователя.

Ух, как много написал. Хоть еще одну статейку выкладывай
Kogrom
Андрей Светлов
А всё потому, что при разработке думали об удобстве автоматического тестирования, забыв главное правило - программа создается для конечного пользователя.
А кто этот конечный пользователь и зачем ему знать, какие параметры мы используем?
Если дело только в параметрах по умолчанию, то можно так сделать:

class Billing:

def _can_show(self, date):
...

def can_show(self):
return self._can_show(datetime.now())
и тестировать _can_show(). Есть некоторый довод для такого подхода - обычно бывает полезно, если код, зависящий от библиотек, выделен в отдельную область.
Андрей Светлов
Когда я говорил о “пользователях” - имел в виду программистов, использующих класс Billing в своей работе.
Написал один человек, а применять должны остальные пять-десять-сто, работающие на том же проекте.

Конечный нажиматель кнопок графического интерфейса, согласитесь, персонаж из другой книги.

Можно писать и _can_show. Это снова вызывает вопросы - но подход хорош тем, что явно завставит подумать - может, так и хотели?
Минусы:
- каждый раз писать по два метода на одно действие?
- вы сознательно сокращаете область покрытия юниттестами.

Я уже собрался привести дополнительные доводы, но они несущественные.

Как я понимаю, мы по разному понимали “пользователя кода”.
Я имел в виду именно программистов.

Использую данный класс как “черный ящик” в соответствии с его public interface и не вникаю в детали реализации/тестирования до тех пор, пока этот код работает так, как я его представляю его функционирование.
Если что-то ломается - читаю внимательно, правлю код и пишу новые тесты.
Kogrom
Андрей Светлов
Минусы:
- каждый раз писать по два метода на одно действие?
- вы сознательно сокращаете область покрытия юниттестами.
Первый минус: не на каждое действие, а в таких хитрых случаях. Но согласен, что это немного усложняет понимание кода.
Второй минус: ну так и с помощью хакерства в тестах мы того же достигаем, если не худшего.

Андрей Светлов
Использую данный класс как “черный ящик” в соответствии с его public interface и не вникаю в детали реализации/тестирования до тех пор, пока этот код работает так, как я его представляю его функционирование.
“Черный ящик” - это удобно и быстро, хотя пропускает взаимоисключающие ошибки, например. Но это так, теория.
Факт в том, что вами было предложено хакерство, которое использует “белый ящик”.
Андрей Светлов
Вероятно, мы несколько иначе смотрим на юниттесты.
Для меня это выглядит примерно так:
- есть код, который хорошо читается сторонним человеком
- и есть юниттесты для этого кода (модуля или класса - в любом случае они хорошо локализованы).

“Внешний” программист использует парадигму “черного ящика”. Он питает надежду, что там “все работает” и не желает вникать в детали.
Есть public api, не выходи за его рамки - и все будет хорошо.

“Внутренний” программист (или наш “внешний” герой, столкнувшийся со странным) знает об этом ящике всё.
Он видит его как “белый”, пишет хакерские тесты (лишь бы они обеспечивали покрытие близкое к идеальному), правит код реализации.
Потом опять пишет тесты. Сознавая грань между “внутренним” и “внешним”. Тесты могут вторгаться в детали реализации.

mock тестирование появилось в ответ на сложности традиционного подхода к созданию юниттестов.
Позвольте привести статью того же Мартина Фаулера: http://martinfowler.com/articles/mocksArentStubs.html

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

Kogrom, спасибо вам за оппозицию. Отвечая на них я смог четче сформулировать мою точку зрения.
Она складывалась не за один день, и я уже успел немного позабыть то первое впечатление от столкновения с подобным подходом:
- эти люди так хачат в своих тестах!
- заявляют, что у них test driven development!
- и после этого кто-то запрещает мне ковыряться в носу!
Naota
Андрей Светлов Честно пример с mocker-ом не понял. Можно в двух словах как это работает? Спасибо.
Kogrom
Андрей Светлов
Вероятно, мы несколько иначе смотрим на юниттесты.
Для меня это выглядит примерно так:
- есть код, который хорошо читается сторонним человеком
- и есть юниттесты для этого кода (модуля или класса - в любом случае они хорошо локализованы).
Да, вероятно, по разному.
Я понимаю так, что юнит-тесты и код, который они тестируют сильно взаимосвязаны, так как:
1. Юнит-тесты дают примеры использования кода. То есть получается своеобразная живая документация. Поэтому пользователю-программисту они очень пригодятся. Без них код будет беднее.
2. Юнит-тесты формируют из кода некий недофрейворк (библиотеку, пакет - называйте как хотите). Как они это делают? Проверяют классы на вторичное использование. И это хорошо. Посмотрите на библиотечные функции и классы Python-а - они избыточны для конкретного программиста, но никто особо не страдает. Поэтому не вижу ничего плохого в том, что юнит-тест диктует коду каким ему быть.
3. До mock-объектов я пока не дорос, использую fake-объекты. Но их я передаю вместо неудобных параметров, а не для переопределения методов (данных) объекта, как в примере: a.now = now.

Ну и в дополнение заметка от Кента Бека:
http://www.threeriversinstitute.org/Testing%20Dichotomies%20and%20TDD.htm

mock-объекты понимаю, но паранойя мешает активно использовать сторонние пакеты, а велосипед делать пока лень. Поэтому довольствуюсь менее элегантными подделками :)

Спасибо за терпение.
Хочу отметить для читающих, что я теоретик и одиночка, поэтому мои взгляды не так весомы, как мнение опытного практика, работающего в команде. Прагматикам не рекомендую верить моим сообщениям :)
Андрей Светлов
Naota, что именно вы не поняли?

Давайте русским текстом проговорим, что мы тестируем.
Объект биллинга запоминает время создания. В течении пяти секунд после этого момента он может показывать свои данные.

Я слегка переписал пример.
from datetime import datetime, timedelta

class Billing(object):
now = datetime.now

def __init__(self):
self.timestamp = self.now()

def can_show(self):
return self.now() - self.timestamp < timedelta(seconds=5)

#### Test
import unittest
import mocker

class TestBillling(unittest.TestCase):
def setUp(self):
self.mocker = mocker.Mocker()

def tearDown(self):
self.mocker = None

def test_can_show(self):
billing = Billing()
now = self.mocker.mock()

stamp = billing.timestamp

billing.now = now

# mocker setup
with self.mocker.order():
# first call - just now
now()
self.mocker.result(stamp)

# after 4 seconds
now()
self.mocker.result(stamp + timedelta(seconds=4))

# after next 4 seconds
now()
self.mocker.result(stamp + timedelta(seconds=8))

# test replay
with self.mocker:
# first call
self.assertEqual(True, billing.can_show())
# second call
self.assertEqual(True, billing.can_show())
# third call
self.assertEqual(False, billing.can_show())


unittest.main()
Mocker сначала настраивается на сценарий работы, а потом проходит по этому сценарию (заодно проверяя, так ли его использовали как сценарий описывал).
Для обманывания биллинга переопределяем class attribute `now` на instance attribute, показывающий на mock object.
Mocker - это именно сценарий, а его `.mock()` объекты выступают в качестве подопытных зверушек.
Переопределять не всегда требуется - часто достаточно передать mock параметром метода - если интерфейс метода загодя конструировался на подобное использование.
В примере сценарий трижды задавал вызов now.__call__. Mocker позволяет настраивать гораздо больше: вызов методов и кучу других операций:
value = mock.attr
mock.attr = value
del mock.attr
mock()
value in mock
mock
mock = value
del mock
len(mock)
bool(mock)
iter(mock)
возвращать значения, генераторы, вызывать пользовательские функции и генерировать исключения.
Список богат, а запись - лаконична.
Прочтите http://niemeyer.net/mocker - документация очень хороша.

Небольшой трюк с заменой class variable на instance variable позволяет не откатывать тестовую настройку в tearDown - изменения касаются только _экземпляра_ тестового объекта.

Вот вроде бы и всё.

mocker - не единственная библиотека такого рода. Мне она нравится больше альтернатив (mock, pmock) - язык создания сценариев должен быть как можно более естественным.
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Powered by DjangoBB