Компоненты с отслеживанием состояния в Clojure: Часть 3 по тестированию
Автор Майкл Гааре
Ранее в Части 1 и Части 2 мы рассмотрели некоторые проблемы с использованием глобальных объектов для компонентов с отслеживанием состояния в Clojure, а также подход к решению этой проблемы с использованием локального состояния с ограниченной областью видимости, управляемого с помощью Библиотеки компонентов Стюарта Сьерры.
Несколько человек отметили, что я не выполнил обещание показать, как этот подход помогает при тестировании. Со стороны может показаться, что я просто забыл включить эту часть, и, поразительно демонстрируя плохие приоритеты, я тратил время на написание беспорядочной последовательности снов. Это может выглядеть так! Но в лучших традициях одного выдающегося политика я смело заявляю, что моя так называемая «ошибка» была полностью преднамеренной - просто частью моего грандиозного плана - и это вы ошибаетесь.
Итак, давайте посмотрим на тестирование этих вещей.
Тест простого веб-обработчика
Помните, что в Части 1 был этот простой тест веб-обработчика, который использует базу данных из глобального состояния:
(deftest homepage-handler-test (with-bindings {app.db/*db-config* test-config} (is (re-find #"Hits: [0-9]." (homepage-handler {})))))
Мы переписали наше соединение с базой данных как запись в стиле Component, реализовав протокол DBQuery, например:
(defprotocol DBQuery (select [db query]) (connection [db])) (defrecord Database [config conn] component/Lifecycle (start [this] ...) (stop [this] ...) DBQuery (select [_ query] (jdbc/query conn query)) (connection [_] conn)) (defn database "Returns Database component from config, which is either a connection string or JDBC connection map." [config] (assert (or (string? config) (map? config))) (->Database config nil))
Мы также переписали наше веб-приложение как компонент, в который вводится запись базы данных, например:
(defrecord WebApp [database]) (defn web-app [] (->WebApp nil)) (defn web-handler [web-app] (GET "/" req (homepage-handler web-app req)))
Это дает нам несколько способов проверить функциональность нашего веб-приложения и обработчика. Во-первых, мы можем продублировать исходную логику тестирования, которая использует тестовую базу данных.
(def test-db (atom nil)) (defn with-test-db [f] (reset! test-db (component/start (database test-config))) (f) (component/stop @test-db)) (use-fixtures :once with-test-db) (deftest homepage-handler-test (let [test-web-app (assoc (web-app) :database @test-db)] (is (re-find #"Hits: [0-9].") (homepage-handler test-web-app {}))))
Если мы не хотим использовать реальную базу данных для тестирования, мы также можем имитировать необходимые функции в фиктивном компоненте базы данных.
(defrecord MockDatabase [select-fn] DBQuery (select [_ query] (select-fn query)) (connection [_] nil)) (defn mock-database "Returns a mock database component that calls select-fn with the passed in query when DBQuery/select is called." [select-fn] (->MockDatabase select-fn)) (deftest homepage-handler-test (let [mock-db (mock-database (constantly {:hits 10})) test-web-app (assoc (web-app) :database mock-db)] (is (re-find #"Hits: 10" (homepage-handler test-web-app {})))))
Тестирование рабочих изображений
Большая часть этого кода может быть повторно использована при тестировании рабочего компонента изображения, который также использует соединение с базой данных. Код этого компонента немного длиннее, поэтому обратитесь к Части 2, если вам нужно освежить свои знания. Для него очень просто написать тест, использующий тестовую базу данных из нашего предыдущего примера.
(deftest image-worker-test (let [... insert some tasks into the test db ... test-image-worker (component/start (assoc (image-worker {:workers 2}) :database @test-db))] (Thread/sleep 2000) (is (... check if the test-db's tasks were completed ... )) (component/stop test-image-worker)))
Также обратите внимание, что мы жестко запрограммировали идею о том, что очередь задач находится в базе данных, в нашем компоненте обработчика изображений. Это тоже можно улучшить, превратив его в отдельный компонент с собственным API протокола.
(defprotocol TaskQueue (next-task [this] "Returns the next task in the queue, or nil if no task is in the queue.") (complete-task [this task] "Sets task as complete.")) (defrecord DBTaskQueue [config db] TaskQueue (next-task [this] (database/select db ...)) (complete-task [this task] (let [conn (database/connection db)] (jdbc/with-transaction ...))))
Это позволяет нам гибко моделировать очередь задач для тестирования компонентов, которые ее используют.
(defrecord MockTaskQueue [tasks] TaskQueue (next-task [this] (let [[nxt & rst :as ts] @tasks] (if (compare-and-set! tasks ts rst) nxt (recur)))) (complete-task [this task] true)) (defn mock-task-queue "Takes collection of tasks, returns mock task queue." [tasks] (->MockTaskQueue (atom tasks))) (deftest image-worker-test (let [tq (mock-task-queue [test-task1 test-task2]) test-image-worker (component/start (assoc (image-worker {:workers 2}) :database tq))] (Thread/sleep 2000) (is (... check if the test task images were processed ...)) (component/stop test-image-worker)))
Одна вещь, которая была упущена при обсуждении в предыдущих статьях, заключалась в том, что обработчики изображений производят побочные эффекты, не связанные с базой данных. Они вызывают функцию `add-watermark`, которая предположительно считывает изображение, выполняет какую-то обработку и где-то повторно сохраняет его. Тестируемость этого тоже может быть улучшена путем преобразования его в компонент, который можно смоделировать или переопределить для тестов обработчика изображений.
Тестовые системы
Можно пойти еще дальше в этом стиле тестирования и построить полные системные карты компонентов для нашего тестирования. Чтобы освежить нашу память, карта компонентов системы - это структура, которая определяет все компоненты в нашей системе, описывает их зависимости и может запускать их по порядку и передавать зависимости для возврата работающей системы.
В Части 2 мы построили систему, которая включает веб-сервер, веб-обработчики и базу данных. Давайте посмотрим на определение общесистемного теста.
(def test-system (component/system-map :db (database test-db-config) :web-app (component/using (web-app) {:database :db}) :web-server (component/using (immutant-server {:port 8990}) [:web-app]))) (deftest system-test (let [started-system (component/start test-system) test-response (http/get "http://localhost:8990/")] (is (re-find #"Hits: [0-9].") (:body test-response)) (component/stop started-system))
Преимущества
Самый естественный способ работы в Clojure - как функциональном языке с фантастическим рабочим процессом, управляемым REPL - это вызывать функции и управлять их поведением, передавая им аргументы. Это улучшает наш рабочий процесс при разработке, а также дает нам гибкие и простые способы тестирования нашего кода.
Стиль кода, управляемый компонентами, обеспечивает такие же преимущества при работе с областями наших систем, которые имеют дело с побочными эффектами, состоянием и внешними системами. Доступ к управлению поведением этих компонентов осуществляется путем передачи им различных аргументов при построении и передачи различных зависимостей. Это дает те же преимущества, что и чистые функции, но при этом изолирует побочные эффекты и состояния.
Резюме
Я написал эту серию, потому что, когда я начинал работать с Clojure, процесс создания чистых систем был болезненным. В сообществе не было единого мнения о том, как управлять ресурсами с отслеживанием состояния, и определенно не было структуры, которую я мог бы использовать. Моей целью было дать руководство, которое я хотел бы иметь; концепции можно было связать вместе, и новички могли испытать радость от Clojure даже раньше, чем я.
В этой серии статей мы рассмотрели несколько стратегий управления ресурсами с отслеживанием состояния в глобальной среде и некоторые проблемы, которые эти подходы вызывают для интерактивной разработки и тестирования. Потом нам приснился кошмар, что нас дополняют котята. Затем мы рассмотрели шаблон проектирования с использованием библиотеки компонентов Стюарта Сьерры для управления ресурсами с отслеживанием состояния в локальной области с внедрением зависимостей. Наконец, мы рассмотрели некоторые стратегии тестирования компонентов, построенных в этом стиле.
Прочитав эту серию, я надеюсь, вы увидите красоту, которую я вижу в создании чистых систем на Clojure с использованием компонентов. Идите и стройте, и пусть котята всегда будут в вашу пользу.
Оригинальная статья в Инженерных лестницах