Статьи

Тестирование API: быстрое, грязное и автоматизированное

При написании RESTful API процесс тестирования может быть простым или отвратительным. REST Client Почтальон довольно популярным для тестирования API. Я уверен, что есть и другие, но я работаю с людьми, которым нравится Почтальон. Почтальон 10 имеет некоторые возможности автоматизации. Немного. Однако. (И это важно.) Это не очень помогает в создании правильного сложного сообщения JSON. При работе с более крупными и сложными API-интерфейсами с более крупными и сложными вложенными и повторяющимися структурами требуется значительно больше помощи, чтобы составить правильный запрос и провести некоторую рациональную оценку ответа.

Введите Python, httplib и json. Хотя Python3 повсеместно лучше, эти библиотеки не сильно изменились со времен Python2, поэтому любая версия будет работать.

Идея проста.

  1. Создайте шаблоны для возможных определений классов в Python. Это может упростить построение структур JSON. Это может сэкономить много надежд на то, что контент JSON правильный. Это может сэкономить время при «предварительном» тестировании, когда структуры JSON неверны. 
  2. Создавайте сложные сообщения, используя определения классов шаблонов.
  3. Отправьте сообщение с помощью httplib. Прочитайте ответ.
  4. Оцените ответы с помощью простого сценария.

Некоторые тестовые сценарии возможны в Почтальоне.
Некоторые . В Python у вас есть полный язык программирования. Определитель «некоторые» испаряется.

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

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

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

Я должен был выкопать мою копию
https://www.packtpub.com/application-development/mastering-object-oriented-python для разработки требуемой хитрости метакласса.

Классы модели и метамодели

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

Наш вариант использования выглядит следующим образом.

>>> class P(Model):
...    attr1= String()
...    attr2= Array()
...
>>> class Q(Model):
...    attr3= String()
...
>>> example= P( attr1="this", attr2=[Q(attr3="that")] )

Наша цель — создать более сложные документы JSON для использования в тестировании API. Понятно, что определения классов слишком узкие, чтобы иметь много реального значения. Это удобные способы определения структуры данных, обеспечивающей минимальный уровень проверки и возможность предоставления значений по умолчанию.

Учитывая эту цель, нам нужны определения класса модели и дескриптора. В дополнение к классу модели нам также понадобится метакласс, который поможет построить необходимые объекты. Одна особенность, которая нам действительно нравится, — это поддерживать атрибуты уровня класса в порядке. Что-то, что Python не делает автоматически. Но кое-что мы можем изобразить через метакласс и порядковый номер на уровне класса в дескрипторах.

Вот метакласс для очистки класса __dict__. Это версия Python2.7, потому что это то, что мы
повторное использование.

class Meta(type):
    """Metaclass to set the ``name`` attribute of each Attr instance and provide
    the ``_attr_order`` sequence that defines the origiunal order.
    """
    def __new__( cls, name, bases, dict ):
        attr_list = sorted( (a_name
            for a_name in dict
            if isinstance(dict[a_name], Attr)), key=lambda x:dict[x].seq )
        for a_name in attr_list:
            setattr( dict[a_name], 'name', a_name )
        dict['_attr_order']= attr_list
        return super(Meta, cls).__new__( cls, name, bases, dict )

class Model(object):
    """Superclass for all model class definitions;
    includes the metaclass to tweak subclass definitions.
    This also provides a ``to_dict()`` method used for
    JSON encoding of the defined attributes.

    The __init__() method validates each keyword argument to
    assure that they match the defined attributes only.
    """
    __metaclass__= Meta
    def __init__( self, **kw ):
        for name, value in kw.items():
            if name not in self._attr_order:
                raise AttributeError( "{0} unknown".format(name) )
            setattr( self, name, value )
    def to_dict( self ):
        od= OrderedDict()
        for name in self._attr_order:
            od[name]= getattr(self, name)
        return od

Метод __new __ () гарантирует, что к каждому определению класса добавлен дополнительный атрибут _attr_order. Метод __init __ () позволяет нам создать экземпляр класса с параметрами ключевого слова, на которые наложена минимальная проверка работоспособности. Метод to_dict () используется для преобразования объекта перед созданием JSON-представления.

Вот определение суперкласса атрибута. Мы расширим это с другими специализациями атрибута.

class Attr(object):
    """A superclass for Attributes; supports a minimal
    feature set. Attribute ordering is maintained via
    a class-level counter.

    Attribute names are bound later via a metaclass
    process that provides names for each attribute.

    Attributes can have a default value if they are
    omitted.
    """
    attr_seq= 0
    default= None
    def __init__( self, *args ):
        self.seq= Attr.attr_seq
        Attr.attr_seq += 1
        self.name= None # Will be assigned by metaclass ``Meta``
    def __get__( self, instance, something ):
        return instance.__dict__.get(self.name, self.default)
    def __set__( self, instance, value ):
        instance.__dict__[self.name]= value
    def __delete__( self, *args ):
        pass

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

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

class String(Attr):
    default= ""

class Array(Attr):
    default= []

class Number(Attr):
    default= None

Последний компонент — кодер JSON, который может обрабатывать эти определения классов. Идея в том, что мы не просим многого от нашего кодировщика. Просто плавный способ превратить эти классы в нужные объекты dict.

class ModelEncoder(json.JSONEncoder):
    """Extend the JSON Encoder to support our Model/Attr
    structure.
    """
    def default( self, obj ):
        if isinstance(obj,Model):
            return obj.to_dict()
        return super(NamespaceEncoder,self).default(obj)

encoder= ModelEncoder(indent=2)

Тестовые случаи

Вот очень важный модульный тестовый пример. Это показывает, как мы можем определить очень простые классы и создать объект из этих определений классов.

>>> class P(Model):
...    attr1= String()
...    attr2= Array()
...
>>> class Q(Model):
...    attr3= String()
...
>>> example= P( attr1="this", attr2=[Q(attr3="that")] )
>>> print( encoder.encode( example ) )
{
  "attr1": "this", 
  "attr2": [
    {
      "attr3": "that"
    }
  ]
}

Учитывая две простые структуры классов, мы можем получить сообщение JSON, которое мы можем использовать для модульного тестирования. Мы можем использовать httplib, чтобы отправить это на сервер и проверить результаты.