Дисклеймер: писать на 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()


Хз что

Скорее всего, это мусор - удаляем его


Запускаем тесты

  1. Создаем venv + ставим pytest - ну по классике

  2. Ставим gae-sdk в venv - оказывается, так можно

  3. Создаем конфиг - pytest.ini / setup.cfg:

    [pytest]
    addops = --doctest-modules
    testpaths = tests any_packet
    
    • addops = --doctest-modules - включаем доктесты
    • К слову: те доктесты, которые не хотим запускать, можно игнорить, используя # doctest: +SKIP в конце строки доктеста
    • testpaths = - поиск тестов в определенных директориях, а не во всем проекте
    • Мы внедряем тесты потихоньку, так что написали всего один прод-пакет - any_packet
  4. Создаем tests/conftest.py:

    from google.appengine.ext import vendor
    
    def pytest_sessionstart(session):
      vendor.add('lib')
    
    • pytest_sessionstart запускается перед всеми тестами
    • vendor.add('lib') добавляет либы из lib/ в PYTHONPATH (чтобы они подсосались в рантайм)
  5. Все, теперь можно одним словом запускать тесты - 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-примитивы - ndb.GeoPt:
  • Тут все просто - делаем фабрику, которая будет создавать примитив, проставляя рандомные координаты с помощью 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')