Дисклеймер: писать на Python 2 и GAE Standard c Python 2 в 2022 - такое себе - у нас просто на работке легаси на этом, и надо как-то вариться в нем.
Как тестировать Python 2 приложухи на GAE?
Сперва, хочу сказать, что я...
но в итоге все ± робит
Итак,
Чего мы хотим от тестирования?
- Легкость написания
- Легкость запуска (руками и автоматически)
Легкость запуска
Сначала надо запустить хоть какой-то код.
Ведь, когда у тебя 7-летнее легаси, которое тестится только в проде, - это непростая задачка
▶, и что получаем?
ЖмемКУЧА КРАСНОГО ТЕКСТА
Но его можно разбить на кучки:
GAE SDK
google.appengine.api.app_identity
, google.appengine.api.modules
Тут всякая инфа о приложении (проекте), типа айди, GCS-бакет - это хардкодим при получении исключения:
app_identity.get_application_id()
> 'project-id'
Google API
googleapiclient.descovery.build
Создает апи-клиент для работы с гугл-сервисами, типа BigQuery
Это дело мокаем при ошибках:
service = mock.MagicMock()
Хз что
Скорее всего, это мусор - удаляем его
Запускаем тесты
-
Создаем venv + ставим pytest - ну по классике
-
Ставим gae-sdk в venv - оказывается, так можно
-
Создаем конфиг -
pytest.ini
/setup.cfg
:[pytest] addops = --doctest-modules testpaths = tests any_packet
addops = --doctest-modules
- включаем доктесты- К слову: те доктесты, которые не хотим запускать, можно игнорить, используя
# doctest: +SKIP
в конце строки доктеста testpaths =
- поиск тестов в определенных директориях, а не во всем проекте- Мы внедряем тесты потихоньку, так что написали всего один прод-пакет -
any_packet
-
Создаем
tests/conftest.py
:from google.appengine.ext import vendor def pytest_sessionstart(session): vendor.add('lib')
pytest_sessionstart
запускается перед всеми тестамиvendor.add('lib')
добавляет либы изlib/
вPYTHONPATH
(чтобы они подсосались в рантайм)
-
Все, теперь можно одним словом запускать тесты -
pytest
- Это же можно запускать на CI / pre-commit
Легкость написания
pytest сам по себе простой - тесты можно писать как функции с assert-ами, но для GAE нужно пару приблуд
Тесты ndb
- GAE SDK содержит утилиты для написания тестов - testbed
- Дружим pytest и testbed, создавая фикстуру:
from google.appengine.ext import ndb, testbed
@pytest.fixture
def mock_ndb():
testbed_ = testbed.Testbed()
testbed_.activate()
testbed_.init_datastore_v3_stub()
testbed_.init_memcache_stub()
ndb.get_context().clear_cache()
yield
testbed_.deactivate()
- Используем ее везде, где тестируются ndb-модельки:
def test_insert_entity(mock_ndb):
TestModel().put()
self.assertEqual(1, len(TestModel.query().fetch(2)))
FactoryBoy + ndb
- Удобно создавать сущности с большой вложенностью с помощью factoryboy
- Есть модификация для ndb - factoryboy-gaendb
- Но она не работает! Я даже форк создал, и все равно не работает. И последний раз либа обновлялась 4 года назад и ваще
- Так что будем дружить factoryboy и ndb самостоятельно
- Дружить будем на примере 2 моделек:
Order
- заказ доставки товаров/еды/чего-угодно иClient
- чел, совершивший заказ
.put()
для сохранения инстанца
Вызов - По умолчанию factoryboy вызывает метод
.save()
- У GAE сущностей, этот методы называется иначе -
.put()
- Какой методы вызвать после создания модели в factoryboy можно, используя
Factory.post_generation
:
from factory import Factory, post_generation
class ClientFactory(Factory):
class Meta:
model = Client
@post_generation
def put(obj, *args, **kwargs):
obj.put()
post_generation
вызывается после создания инстанца фабрики, то есть послеClientFactory()
ndb.KeyProperty
Связи с другими моделями - - Для того чтобы связать 2 модели, нужно при создании
Order
создаватьClient
, и проставлятьClient.key
вOrder
- Если в Django достаточно просто
factory.SubFactory
, то в случае GAE нам нужно получить.key
у сущности, которая создалать черезSubFactory
, а больше нам сущность не нужна - Для этого используем 2 хитрости:
Params
иLazyAttribute
:
class OrderFactory(Factory):
class Meta:
model = Order
class Params:
client = SubFactory(ClientFactory)
client = factory.LazyAttribute(lambda o: o.client.key)
Params
позволяет создавать сущности / данные без передачи в контруктор модели фабрики- К слову:
Params
можно переопредять в конструкторе фабрики LazyAttribute
позволяет брать не всю сущность, а лишь одно значение - а это то что нам нужно дляKeyProperty
ndb.GeoPt
:
ndb-примитивы - - Тут все просто - делаем фабрику, которая будет создавать примитив, проставляя рандомные координаты с помощью Faker :
from factory import Faker, Factory
from google.appengine.api.datastore_types import GeoPt
class GeoPtFactory(Factory):
class Meta:
model = GeoPt
lat = Faker('latitude')
lon = Faker('longitude')
- Важно: factoryboy интегрируется с Faker, так что импортировать Faker нужно из
factory