Рубрики
Без рубрики

Блок тестирования приложений с колбами Пимонго с монгомоком и патчами

Устройство тестирования приложений для колба Pymongo с введением Mongomock Это… Теги с Python, тестированием, MongoDB, учебником.

Введение

Это нишевое руководство. Причина этого руководства заключается в том, что во время проекта мне нужно было найти способ проверить простое приложение Fymongo Crud Pymongo. При попытке тестировать, я обнаружил небольшую онлайн-документацию в отношении того, как Mock Mongo в рамках моих модульных тестов, и не нашел ответов на Stackoverflow, который легко работал.

Я нашел руководства, используя Pтойцы, и руководства, которые были для приложений для колб, которые использовали Mongoclient и подключение. Ни один из которых не удовлетворил установку, которую я использовал. Были также тестирование решений, использующих MOCKUPDB, но они казались чрезмерно сложными.

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

Специфический стек, который я использую:

  • Python3.
  • Колбы
  • Пимонго (используя init_app)
  • Модульный тест

Заявление

Приложение, которое мы намереваемся создать, это простое приложение CRUD, сделанное в колбе, с базой данных MongoDB. Мы рассмотрим конечные точки для создания, извлечения и удаления ресурсов «статьи». Содержание этих ресурсов – это просто «автор», «контент» и список «тегов».

Весь код для этого проекта можно найти в репозитории GitHub: https://github.com/reritom/flask-pymongo-unittestes-guide. .

Расположение каталога

Мы будем следовать наилучшей практикой в отношении установки репо. Будет Src. каталог для нашего исходного кода, A Тесты Каталог для наших тестов, и приложение номинально будет развернуто под управлением Python Main.py. .

Наш каталог будет выглядеть так (игнорируя readme, требования. Atxt и другие разные файлы):

.
├── main.py
├── src
│   ├── __init__.py
│   ├── application.py
│   ├── database.py
│   └── controllers
│       ├── __init__.py
│       └── article_controller.py
└── tests
    ├── __init__.py
    └── articles_test.py

Установка и бега

Установка

Если вы заинтересованы в тестировании кода. Создать виртуальную среду, запустите PIP Установка -R требования. atxt Отказ

Бегущие тесты

Чтобы запустить тесты, вы можете использовать Python -M Unittest Discover -P '* _test.py' Отказ

Источник

Если вы работали с колбой раньше, вы будете знать, что обычно есть application.py (Или иногда вы положите это в __init__.py SRC). Тогда вы создаете База данных .py который будет содержать ваш объект базы данных. Когда вы создаете свое первое приложение Flask, часто вы поставьте объект базы данных в свой application.py , но как только вы начнете расщеплять свое приложение, вы столкнулись с круговыми импортными проблемами, если база данных существует в вашем application.py Отказ Итак, мы создаем два файла: База данных .py , application.py Отказ

База данных .py достаточно просто. Начинается, выглядя так:

# src/database.py
from flask_pymongo import PyMongo

mongo = PyMongo()

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

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

application.py будет выглядеть так:

# src/application.py
from flask import Flask
from src.database import mongo
from src.controllers import ArticleController

def create_app(db_uri: str) -> Flask:
    app = Flask(__name__)
    app.config["MONGO_URI"] = db_uri
    mongo.init_app(app)

    # Add the articles collection if it doesn't already exist
    if not 'articles' in mongo.db.list_collection_names():
        articles_collection = mongo.db['articles']

    # Register the article routes
    app.add_url_rule("/articles", methods=["POST"], view_func=ArticleController.create_article)
    app.add_url_rule("/articles", methods=["GET"], view_func=ArticleController.get_articles)
    app.add_url_rule("/articles/", methods=["GET"], view_func=ArticleController.get_article)
    app.add_url_rule("/articles/", methods=["DELETE"], view_func=ArticleController.delete_article)

    return app

Вы замечаете, что у нас есть класс под названием ArticleController В подкаталоге контроллера. Причина, по которой мы можем импортировать его непосредственно из модуля контроллеров, а не импортировать его вроде От SRC.Controllers.article_Controller Импорт ArticleController , потому что мы импортировали контроллер в SRC/Контроллеры/__ init__.py Отказ

ArticleController на данный момент просто сосредотачивается на аспекте create_article. Для создания статьи мы не сделаем никакой проверки. Вместо этого мы просто принимаем контент из запроса, храните его в нашей коллекции Mongo и верните данные из запроса вместе с идентификатором документа Mongo, который напоминает это:

# src/controllers/article_controller.py
import uuid
from flask import request, jsonify
from src.database import mongo

class ArticleController:
    @staticmethod
    def create_article():
        """
        Take the article from the request and deposit it directly into our mongo collection
        """
        data = request.get_json(force=True)
        mongo.db.articles.insert_one(data)
        data["_id"] = str(data["_id"]) # The mongo-added id isn't serialisable, so we convert it to a string
        return jsonify(data), 201

    @staticmethod
    def get_article(article_id: uuid.UUID):
        ...

    @staticmethod
    def get_articles():
        ...

    @staticmethod
    def delete_article(article_id: uuid.UUID):
        ...

Наконец, чтобы запустить это приложение, мы создаем main.py , который может быть всего лишь несколько строк в этом случае.

# main.py
from src import create_app

if __name__=="__main__":
    db_uri = "mongodb://'127.0.0.1:27017/mydatabase"
    app = create_app(db_uri)
    app.run("0.0.0.0", port=5000, debug=False)

Опять обратите внимание, что create_app можно импортировать непосредственно из SRC, потому что в SRC/__ init__.py Я добавил От SRC.Application Import Create_app Отказ Это во многом только для удобства, когда проекты растут и становятся более вложенными.

Теперь, только с этими четырьмя файлами мы можем запустить наше приложение и создавать статьи, публикующие данные на /Статьи Конечная точка, предполагая, что вы используете MongoDB на заднем плане и пропустите правильный DB_URI в функцию create_app.

Тестирование

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

При тестировании мы ориентируемся на код, и наш тест должен быть отделен от Mongod так, что тесты могут быть проще проще, и поэтому они не зависят от службы Mongod.

Подход, который мы принимаем для этого, называется «насмешкой». Мы хотим отправиться в Mock Mongo, которое похоже на создание фальшивого экземпляра Mongo, который, кажется, действует так же, как настоящий монго (хотя, очевидно, сходство обычно – глубоко).

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

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

Исправление

Исправления – это способ замены объекта в пространстве имен программы с чем-то другим. Часто это используется для исправления окружающей среды или вызовов Patch API. Представьте, что у вас есть скрипт, который работает так, если Os.environ [«Флаг»] и другой способ, если Os.environ [«Флаг»] Отказ Вы хотели бы создать два теста, один для каждого случая, а затем исправляете OS.environ, чтобы установить флаг в правильное значение для каждого теста.

Важным следует отметить, что при исправлении, состоит в том, что вы можете либо исправить атрибут объекта или исправлять объект в пространстве имен модуля, который потребляет объект (они эффективно одинаковы). Что это значит? Ну, когда вы используете Os.environ, вы импортируете ОС Отказ Эффективно, это означает, что в вашем модуле сейчас есть объект модуля под названием ОС , и это то, что вы хотите исправить, потому что это то, что ваш код потребляет при использовании Os.environ позже.

Практически это выглядит следующим образом:

# dummy.py
import os
def print_flag():
  print(os.environ.get("FLAG"))
# dummy_test.py
from unittest.mock import patch
from dummy import print_flag

# If we patch "os", we are patching it in the wrong namespace, so we can't control what will be printed.
with patch("os") as dummy_os:
  print_flag()

with patch("dummy.os") as dummy_os:
  dummy_os.environ.get.return_value = True
  print_flag() # This will print True, because we have patched the os in the namespace of dummy, and explicitly told the mock object (dummy_os) to return True when os.environ.get(...) is called.

В этом случае при тестировании мы могли исправить каждый случай, когда мы импортируем Монго от app.database. . В пространстве имен каждого потребителя мы могли бы заменить Монго (Mongoclient) объект с нашими Монгомол. Mongoclient Отказ

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

Видя как все потребители базы данных импортируют Mongo из App.database, было бы удобно, если бы мы могли исправить Mongo внутри app.database. модуль. Затем все потребители могут продолжать импортировать этот объект, будучи неиспортом. В наших тестах нам нужно было бы только удостовериться, что объект базы данных издевается, что означает, что наше приложение растет, наши тесты все равно будут действительны.

Затем мы могли бы рассмотреть это как наше src.database Импорт Пимонго мы могли бы высмеивать Пимонго В пространстве имен src.database. .

В вашем тесте вы можете попробовать что-то вроде следующего:

import unittest
from unittest.mock import patch
from src import create_app
import mongomock

class TestApplication(unittest.TestCase):
  def test_application(self):
    with patch("src.database.PyMongo", side_effect=mongomock.MongoClient):
      # Create the app and run the tests
      ...

Теперь вышеуказанный код будет насмехаться Пимонго обратиться к Монгомол. Mongoclient , но ваш тест все равно потерпит неудачу. Это потому, что src.database Модуль уже загружен до запуска вашего теста. Так да, Пимонго теперь относится к Монгомол. Mongoclient , но твои Монго Переменная назначается экземпляру Пимонго Потому что это было беги до насмешка. Так что вы издеваетесь в классе, но слишком поздно.

Затем вы могли бы подумать, либо пытаясь издеваться на src.database Модуль перед рукой, или исправления src.database.mongo объект с нашим Монгомол. Mongoclient пример.

Если мы рассмотрим последнее, мы можем сделать это с некоторыми изменениями в нашем коде. Что мы хотим, чтобы издеваться на Монго объект в src.database. Таким образом, вместо того, чтобы ссылаться на экземпляр Пимонго Теперь теперь относится к экземпляру Монгомол. Монтоблиент . Теперь нам нужно запомнить пространства имен. В обоих SRC/Application.py и SRC/Контроллеры/Article_Controllers.py мы импортируем Mongo . Это означает, что в каждом из этих пространств имен у них уже есть ссылка на Mongo . Так что если мы тогда патча Монго в src.database Модуль, это не будет отражено в Монго Объект, который существует в этих двух пространствах имен. Так что изменение кода нам нужно было бы сделать, это не импортировать Монго в эти два модуля, а вместо этого импортировать app.database. Модуль и доступ Mongo с помощью app.database.mongo Отказ

Эти изменения хотели бы этого:

# src/application.py
from flask import Flask
from src.controllers import ArticleController
import src.database

def create_app(db_uri: str) -> Flask:
    app = Flask(__name__)
    app.config["MONGO_URI"] = db_uri
    src.database.mongo.init_app(app)

    # Add the articles collection if it doesn't already exist
    if not 'articles' in src.database.mongo.db.list_collection_names():
        articles_collection = src.database.mongo.db['articles']

    # Register the article routes
    ...

    return app

а также

# src/controllers/article_controller.py
from flask import request, jsonify
import src.database

class ArticleController:
    @staticmethod
    def create_article():
        data = request.get_json(force=True)
        src.database.mongo.db.articles.insert_one(data)
        data["_id"] = str(data["_id"]) # The mongo-added id isn't serialisable, so we convert it to a string
        return jsonify(data), 201
    ...

В нашем тесте мы можем затем исправить app.database Модуль объекта, так что Монго Относится к нашему Монгомол. Mongoclient Экземпляр, а не Пимонго Отказ

# tests/articles_test.py
import unittest
from unittest.mock import patch
from src import create_app
import src.database
import mongomock

class TestApplication(unittest.TestCase):
  def test_application(self):
    with patch.object(src.database, "mongo", mongomock.MongoClient()):
      # Create the app and run the tests
      ...

На данный момент мы исправляем правильный объект в правильном пространстве имен и потребителям Монго Получаем наш исправленный ресурс. Тем не менее, flask_pymongo. Пимонго и Монгомол. Mongoclient не ссылаются на тот же тип объекта. Пимонго это суперкласс Монтоблиент . Так что вы получите эту ошибку:

Traceback (most recent call last):
  File "/Users/***/projects/flask-pymongo-unittest-guide/tests/articles_test.py", line 20, in test_create_article
    app = create_app("mongodb://localhost:27017/mydatabase").test_client()
  File "/Users/***/projects/flask-pymongo-unittest-guide/src/application.py", line 8, in create_app
    src.database.mongo.init_app(app)
TypeError: 'Database' object is not callable

Или если вы исправлены с Монгомол. Mongoclient , а не Монгомол. Mongoclient () Вы получите эту ошибку.

Traceback (most recent call last):
  File "/Users/***/projects/flask-pymongo-unittest-guide/tests/articles_test.py", line 20, in test_create_article
    app = create_app("mongodb://localhost:27017/mydatabase").test_client()
  File "/Users/***/projects/flask-pymongo-unittest-guide/src/application.py", line 8, in create_app
    src.database.mongo.init_app(app)
AttributeError: type object 'MongoClient' has no attribute 'init_app'

Последняя ошибка – ошибка в вашем коде, и как только вы его исправите, вы получите вместо этого первая ошибка. Чтобы справиться с этим, мы сможем создать фиктивное суперкласс, у которого есть init_app Метод, и мы можем исправить Mongo с этим вместо этого:

# tests/articles_test.py
import unittest
from unittest.mock import patch
from src import create_app
import src.database
from mongomock import MongoClient

class PyMongoMock(MongoClient):
    def init_app(self, app):
        return super().__init__()

class TestApplication(unittest.TestCase):
  def test_application(self):
    with patch.object(src.database, "mongo", PyMongoMock()):
      # Create the app and run the tests
      ...

Обратите внимание, что мы исправляем src.database.mongo С экземпляром Пимонгомока , а не класс.

В этот момент ваш тест сможет успешно работать с издеванным экземпляром Mongo . Вы можете проверить конкретные тесты, которые я написал для этого руководства в репо.

Patching – это очень мощный инструмент в Python, а в Unittest Framework, и писать сильные, автономные модульные тесты – это то, как вы гарантируете, что ваше приложение ведет себя как ожидалось. Сочетание исчезновения и пространств имен можно запутать, и я видел много тестов, где окружающая среда была исправлена неправильно, что приведет к прохождению тестов, но, скорее всего, потерпят неудачу на других машинах, поэтому ее очень важная часть Python о.

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

Оригинал: “https://dev.to/reritom/unit-testing-pymongo-flask-applications-with-mongomock-and-patches-1m23”