Форум сайта python.su
А я вам о чем?
Офлайн
У меня вопрос по теме. Корректно ли править сам класс, чтобы он не зависел от времени? Как-то так:
class Billing:
def can_show(self, date=None):
if not date:
date = datetime.now()
...
Офлайн
Вы задали неодноздачный вопрос.
Исходите из того, какая сигнатура требуется системе “для обычной работы”.
Если ваш Billing предполагает, что в нормальном режиме (рабочем коде его использования) can_show иногда будет вызываться с указанием даты - отлично!
Так и поступайте!
Публичный интерфейс класса должен выглядеть максимально удобно для пользователя.
Если для тестирования требуются хитрости - это заботы тестирования.
Как я написал чуть выше - запомнить datetime.now (функцию, а не возвращаемое значение) как атрибут класса.
Пользователь ничего не заметит и тестирующий код писать станет легче.
И не следует указывать в параметрах по умолчанию те, которые нужны только для юниттестов.
Возьмем ваш же пример. Если Billing.can_show всегда в пользовательском коде вызывается без параметров - не нужно добавлять их только для тестирования.
Сигнатура методов - это та штука, с которой работают все программисты проекта. Чем сигнатуры проще - тем легче код сопровождать.
Мне случалось работать над проектом, в котором приличная часть параметров “по умолчанию” нужна была только для тестов.
Это очень неуютно.
Если параметр есть - значит, его кто-то использует.
Начинаю спрашивать у программистов и аналитиков: а какой-такой бизнес-процесс требует указания даты в can_show? Никто не может ответить.
Паника усиливается: есть часть кода, о которой никто не может сказать - зачем она?
Поиском нахожу все вызовы метода в рабочем коде. Они этот параметр не используют. Тесты - да.
Тесты вообще с кодом творят страшные вещи - им бы только отработать нормально :) А тестов у нас много, тысяч надцать.
Юниттесты еще белые и пушистые, всё на виду. А функциональные да регрессионные - полный ахтунг, черт ногу сломит. А то и башку расшибет.
Может, я плохо искал? Питон ведь очень динамический язык!
В отчаянии обращаюсь к начальнику отдела. Он морщит лоб и устраивает короткое совещание еще с тремя начальниками.
Эти достойные люди не могут понять, в чем дело.
Затем одного осеняет - да! Это мы сделали так, чтобы тесты писать было легче! А потом напрочь забыли.
Решение вылилось в довольно много человекочасов (моих и сотрудников).
А всё потому, что при разработке думали об удобстве автоматического тестирования, забыв главное правило - программа создается для конечного пользователя.
Ух, как много написал. Хоть еще одну статейку выкладывай
Офлайн
Андрей СветловА кто этот конечный пользователь и зачем ему знать, какие параметры мы используем?
А всё потому, что при разработке думали об удобстве автоматического тестирования, забыв главное правило - программа создается для конечного пользователя.
class Billing:
def _can_show(self, date):
...
def can_show(self):
return self._can_show(datetime.now())
Офлайн
Когда я говорил о “пользователях” - имел в виду программистов, использующих класс Billing в своей работе.
Написал один человек, а применять должны остальные пять-десять-сто, работающие на том же проекте.
Конечный нажиматель кнопок графического интерфейса, согласитесь, персонаж из другой книги.
Можно писать и _can_show. Это снова вызывает вопросы - но подход хорош тем, что явно завставит подумать - может, так и хотели?
Минусы:
- каждый раз писать по два метода на одно действие?
- вы сознательно сокращаете область покрытия юниттестами.
Я уже собрался привести дополнительные доводы, но они несущественные.
Как я понимаю, мы по разному понимали “пользователя кода”.
Я имел в виду именно программистов.
Использую данный класс как “черный ящик” в соответствии с его public interface и не вникаю в детали реализации/тестирования до тех пор, пока этот код работает так, как я его представляю его функционирование.
Если что-то ломается - читаю внимательно, правлю код и пишу новые тесты.
Офлайн
Андрей СветловПервый минус: не на каждое действие, а в таких хитрых случаях. Но согласен, что это немного усложняет понимание кода.
Минусы:
- каждый раз писать по два метода на одно действие?
- вы сознательно сокращаете область покрытия юниттестами.
Андрей Светлов“Черный ящик” - это удобно и быстро, хотя пропускает взаимоисключающие ошибки, например. Но это так, теория.
Использую данный класс как “черный ящик” в соответствии с его public interface и не вникаю в детали реализации/тестирования до тех пор, пока этот код работает так, как я его представляю его функционирование.
Офлайн
Вероятно, мы несколько иначе смотрим на юниттесты.
Для меня это выглядит примерно так:
- есть код, который хорошо читается сторонним человеком
- и есть юниттесты для этого кода (модуля или класса - в любом случае они хорошо локализованы).
“Внешний” программист использует парадигму “черного ящика”. Он питает надежду, что там “все работает” и не желает вникать в детали.
Есть public api, не выходи за его рамки - и все будет хорошо.
“Внутренний” программист (или наш “внешний” герой, столкнувшийся со странным) знает об этом ящике всё.
Он видит его как “белый”, пишет хакерские тесты (лишь бы они обеспечивали покрытие близкое к идеальному), правит код реализации.
Потом опять пишет тесты. Сознавая грань между “внутренним” и “внешним”. Тесты могут вторгаться в детали реализации.
mock тестирование появилось в ответ на сложности традиционного подхода к созданию юниттестов.
Позвольте привести статью того же Мартина Фаулера: http://martinfowler.com/articles/mocksArentStubs.html
“Хаки” и переход на “белый ящик” позволяют писать тесты удобней, понятней и проще.
Конечно, в любом деле нужно знать меру. Подмена в тесте рабочего кода на заглушку, всегда возвращающую “все отлично” - нехорошо.
Kogrom, спасибо вам за оппозицию. Отвечая на них я смог четче сформулировать мою точку зрения.
Она складывалась не за один день, и я уже успел немного позабыть то первое впечатление от столкновения с подобным подходом:
- эти люди так хачат в своих тестах!
- заявляют, что у них test driven development!
- и после этого кто-то запрещает мне ковыряться в носу!
Офлайн
Андрей Светлов Честно пример с mocker-ом не понял. Можно в двух словах как это работает? Спасибо.
Офлайн
Андрей СветловДа, вероятно, по разному.
Вероятно, мы несколько иначе смотрим на юниттесты.
Для меня это выглядит примерно так:
- есть код, который хорошо читается сторонним человеком
- и есть юниттесты для этого кода (модуля или класса - в любом случае они хорошо локализованы).
Офлайн
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()
Офлайн