Подтвердить что ты не робот

Как мне создавать свои классы для более легкого модульного тестирования?

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

Чтобы упростить, я задаю вопрос, как настроить систему, в которой вы можете управлять любым количеством сторонних модулей с обновлениями CRUD. Эти сторонние модули - все RESTful API, и данные ответа хранятся в локальных копиях. Что-то вроде удаления учетной записи пользователя должно было бы инициировать удаление всех связанных модулей (которые я называю поставщиками). Эти поставщики могут иметь зависимость от другого провайдера, поэтому порядок удаления/создания важен. Мне интересно, какие шаблоны проектирования я должен использовать для поддержки моего приложения.

Регистрация охватывает несколько классов и хранит данные в нескольких таблицах db. Здесь порядок разных поставщиков и методов (они не статики, просто написаны так для краткости):

  • Provider::create('external::create-user') инициирует регистрацию на определенном этапе конкретного провайдера. Синтаксис двойного двоеточия в первом параметре указывает, что класс должен инициировать создание на providerClass::providerMethod. Я сделал общее предположение, что Provider будет интерфейсом с методами create(), update(), delete(), которые все остальные провайдеры будут реализовывать. Как это создается, скорее всего, вам нужно помочь.
  • $user = Provider_External::createUser() создает пользователя во внешнем API, возвращает успех, и пользователь получает его в моей базе данных.
  • $customer = Provider_Gapps_Customer::create($user) создает клиента в стороннем API, возвращает успех и сохраняет локально.
  • $subscription = Provider_Gapps_Subscription::create($customer) создает подписку, связанную с ранее созданным клиентом в стороннем API, возвращает успех и сохраняет локально.
  • Provider_Gapps_Verification::get($customer, $subscription) извлекает строку из внешнего API. Эта информация хранится локально. Другой звонок, который я пропускаю, чтобы держать вещи краткими.
  • Provider_Gapps_Verification::verify($customer, $subscription) выполняет внешний процесс проверки API. Результат которого хранится локально.

Это действительно подавленный образец, поскольку фактический код использует по меньшей мере 6 внешних вызовов API и более 10 локальных строк базы данных, созданных во время регистрации. Не имеет смысла использовать инъекцию зависимостей на уровне конструктора, потому что мне может потребоваться создать экземпляр 6 классов в контроллере, не зная, нужны ли они мне всем. То, что я хочу выполнить, было бы чем-то вроде Provider::create('external'), где я просто указываю начальный шаг, чтобы начать регистрацию.


Ключ проблемы

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

Я хотел бы создать эту систему таким образом, чтобы указать порядок операций (шагов) при запуске создания нового поставщика. Другими словами, позвольте мне указать, какая комбинация провайдеров/методов запускается следующей в цепочке событий, поскольку создание может охватывать так много шагов. В настоящее время у меня есть эта цепочка событий, происходящих через шаблон subject/observer. Я хочу потенциально переместить этот код в таблицу базы данных, provider_steps, где я перечисляю каждый шаг, а также следующий success_step и failure_step (для откатов и удалений). Таблица выглядит следующим образом:

  # the id of the parent provider row
  provider_id int(11) unsigned primary key,
  # the short, slug name of the step for using in codebase
  step_name varchar(60),
  # the name of the method correlating to the step
  method_name varchar(120),
  # the steps that get triggered on success of this step
  # can be comma delimited; multiple steps could be triggered in parallel
  triggers_success varchar(255),
  # the steps that get triggered on failure of this step
  # can be comma delimited; multiple steps could be triggered in parallel
  triggers_failure varchar(255),
  created_at datetime,
  updated_at datetime,
  index ('provider_id', 'step_name')

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

Кроме того, где было бы лучшее место для запросов db?

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

Вещи, которые я читал...

4b9b3361

Ответ 1

Вы уже работаете с шаблоном pub/sub, который кажется уместным. Учитывая только ваши комментарии выше, я рассматривал бы упорядоченный список как приоритетный механизм.

Но все равно не пахнет правильным, что каждый абонент занимается порядком операций своих иждивенцев для запуска успеха/неудачи. Зависимости обычно выглядят так, как будто они принадлежат дереву, а не списку. Если вы сохранили их в дереве (используя составной шаблон), то встроенная рекурсия сможет очистить каждую зависимость, сначала очистив ее иждивенцы. Таким образом, вы больше не беспокоитесь о приоритизации, в каком порядке происходит очистка - автоматически обрабатывается дерево.

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

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

Одна вещь, которую вы знаете, что вы хотите сделать, - это добавить поставщика, поэтому тест TestAddProvider() кажется подходящим. Обратите внимание, что это должно быть довольно просто на данный момент и не имеет ничего общего с составным шаблоном. После этого вы знаете, что у провайдера есть зависимость. Создайте тест TestAddProviderWithDependent() и посмотрите, как это происходит. Опять же, это не должно быть сложным. Затем вы, скорее всего, захотите использовать TestAddProviderWithTwoDependents() и то, где список будет реализован. Как только вы работаете, вы знаете, что вы хотите, чтобы провайдер также был зависимым, поэтому новый тест докажет, что модель наследования работает. Оттуда вы добавили достаточное количество тестов, чтобы убедить себя в том, что сработали различные комбинации добавления поставщиков и иждивенцев, а также тесты для условий исключения и т.д. Просто из тестов и требований вы быстро получите сложный шаблон, который соответствует вашим потребностям, В этот момент я действительно вскрыл свою копию GoF, чтобы убедиться, что я понял последствия выбора составного шаблона и убедиться, что я не добавил неуместную бородавку.

Другим известным требованием является удаление поставщиков, поэтому создайте тест TestDeleteProvider() и реализуйте метод DeleteProvider(). Вы не будете далеко от того, чтобы провайдер также удалял своих иждивенцев, поэтому следующим шагом может быть создание теста TestDeleteProviderWithAdependent(). Рекурсия составного шаблона должна быть очевидной на этом этапе, и вам нужно будет только несколько тестов, чтобы убедить себя, что глубоко вложенные поставщики, пустые листы, широкие узлы и т.д. Все будут правильно очищаться.

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

Вы можете обнаружить, что провайдеры должны вызываться в определенном порядке. На этом этапе вам может потребоваться добавить приоритет в списки в каждом node в составном дереве. Или, может быть, вам нужно создать совершенно другую структуру (например, связанный список), чтобы вызвать их в правильном порядке. Используйте тесты и медленно подходите к ним. У вас могут быть люди, которые обеспокоены тем, что вы удаляете иждивенцев в определенном предписанном извне порядке. На этом этапе вы можете использовать свои тесты, чтобы доказать сомневающимся, что вы всегда удаляете их безопасно, даже если не в том порядке, о котором они думали.

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

Тогда возникают сложные вопросы. Что делать, если у вас есть два поставщика, которые имеют общую зависимость? Если вы удаляете одного провайдера, должны ли он удалять все его зависимости, хотя другой поставщик нуждается в одном из них? Добавьте тест и выполните свое правило. Я полагаю, что я бы справился с этим путем подсчета ссылок, но, возможно, вам нужна копия поставщика для второго экземпляра, так что вам никогда не придется беспокоиться о том, чтобы делиться с детьми, и вы делаете вещи проще. Или, может быть, это никогда не проблема в вашем домене. Другой сложный вопрос: если ваши провайдеры могут иметь круговые зависимости. Как вы гарантируете, что не закончите цикл саморегуляции? Напишите тесты и выясните.

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

Это подход, который я бы рассмотрел. Это может быть неправильно для вас, но это вам решать.

Ответ 2

Тестирование устройств При модульном тестировании мы хотим только протестировать код, составляющий отдельную единицу исходного кода, обычно метод или функцию класса в PHP (Обзор тестирования устройства). Что указывает на то, что мы не хотим проверять внешний API в модульном тестировании, мы хотим протестировать код, который мы пишем локально. Если вы хотите протестировать все рабочие процессы, скорее всего, вы захотите выполнить интеграционное тестирование (Обзор тестирования интеграции), который является другим зверем.

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

Stub Out Практика замены объекта тестовым двойником, который (необязательно) возвращает настроенные возвращаемые значения, называется обрезкой. Вы можете использовать заглушку для "замены реального компонента, на котором зависит SUT, чтобы у теста была контрольная точка для косвенных входов SUT. Это позволяет тесту принудительно удалять пути SUT, которые он мог бы иначе не выполнять". Ссылка и примеры

Макетные объекты Практика замены объекта тестовым двойником, который проверяет ожидания, например, утверждая, что метод был вызван, называется насмешкой.

Вы можете использовать макет-объект "в качестве точки наблюдения, которая используется для проверки косвенных выходов SUT, как это выполняется. Обычно макет-объект также включает в себя функциональность тестового заглушки, поскольку он должен возвращать значения в SUT, если он еще не пропустил тесты, но акцент делается на проверке косвенных выходов. Поэтому макет-объект - это нечто большее, чем просто тестовая заглушка плюс утверждения, она используется по-разному". Ссылка и примеры

Наши советы Создайте свой класс как для всех, так и для Stubbing и Mocking. Руководство по модулю PHP имеет отличный пример Stubbing и Mocking Web Service. Хотя это не поможет вам разобраться, оно демонстрирует, как вы собираетесь внедрять то же самое для Restful API, который вы используете.

Где лучшее место для запросов db? Мы предлагаем вам использовать ORM и не решать это самостоятельно. Вы можете легко использовать Google PHP ORM и самостоятельно принимать решения исходя из ваших собственных потребностей; наш совет - использовать Doctrine, потому что мы используем Doctrine, и это соответствует нашим потребностям хорошо, и за последние несколько лет мы пришли к пониманию того, насколько хорошо разработчики Doctrine знают домен, просто ставят, они делают это лучше, чем мы могли бы сделать это сами, поэтому мы счастливы позволить им сделать это для нас.

Если вы действительно не понимаете, почему вы должны использовать ORM, см. Почему вы должны использовать ORM?, а затем Google тот же вопрос. Если вы все еще чувствуете, что можете катить свой ORM или иначе обращаться с Access Access лучше, чем ребята, посвященные этому, мы ожидаем, что вы уже знаете ответ на вопрос. Если вы чувствуете, что у вас есть насущная потребность справиться с этим самостоятельно, мы предлагаем вам посмотреть исходный код для числа a из ORM (См. Doctrine on Github) и найдите решение, которое наилучшим образом соответствует вашему сценарию.

Спасибо, что задали интересный вопрос, я ценю его.

Ответ 3

Каждое отношение зависимостей внутри вашей иерархии классов должно быть доступно из внешнего мира (не должно быть сильно связано). Например, если вы создаете экземпляр класса A в классе B, класс B должен иметь методы setter/getter, реализованные для владельца экземпляра класса A в классе B.

http://en.wikipedia.org/wiki/Dependency_injection

Ответ 4

Дальнейшая проблема, которую я вижу с вашим кодом - и это мешает вам проверить ее на самом деле - использует вызовы метода static class:

  • Provider::create('external::create-user')
  • $user = Provider_External::createUser()
  • $customer = Provider_Gapps_Customer::create($user)
  • $subscription = Provider_Gapps_Subscription::create($customer)
  • ...

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

После того, как вы преобразовали все статические вызовы в вызовы метода объекта и использовали Dependency Injection вместо статического глобального состояния для передачи объектов, вы можете просто выполнить модульное тестирование с помощью PHPUnit, в т.ч. используя окурки и макеты, сотрудничающие в ваших (простых) тестах.

Итак, вот TODO:

  • Рефакторинг статических методов вызывает вызовы метода объекта.
  • Используйте Dependency Injection для передачи объектов.

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

Ответ 5

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

Далее вы можете прочитать о функциях/функциональных объектах, особенно Closure, Predicate, Transformer и Supplier, чтобы создать участвующие компоненты. Надеюсь, что это поможет.

Ответ 6

Вы посмотрели шаблон состояния? http://en.wikipedia.org/wiki/State_pattern Вы можете сделать все свои шаги как разные состояния на конечной машине, и это будет выглядеть как граф. Вы можете сохранить этот график в своей таблице базы данных /xml, а также у каждого провайдера может быть свой собственный граф, который представляет порядок выполнения выполнения.

Поэтому, когда вы попадаете в определенное состояние, вы можете инициировать событие/события (сохранить пользователя, получить пользователя). Я не знаю вашего приложения, но события могут быть повторно использованы другими поставщиками.

Если на некоторых шагах сработает, то выполняется другой путь графа.

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

Затем, если вам нужно добавить другого провайдера, вам нужно создать граф и/или несколько новых событий.

Вот пример: https://github.com/Metabor/Statemachine