Mock-объекты (объекты-заглушки) представляют собой фиктивную реализацию части программы или ее окружения, например, вызов стороннего сервиса. Mock-объекты, в частности mock-сервисы активно используются при модульном тестировании, когда нужно имитировать часть окружения тестируемого модуля.

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

Сервис-заглушка может нести внутри себя какую угодно логику. Наш будет очень простым – сервис будет отдавать в ответе содержимое определенного файла (нашего ответа по умолчанию), лежащего на диске. Соответственно, управлять ответом можно будет через изменение содержимого данного файла.

Реализация

Было принято решение реализовать mock-сервис на на языке Python 3.5 с использованием фреймворка flask. Flask был выбран из-за простоты написания и деплоя сервисов.

Устанавливается flask так же, как и все сторонние модули python. В командной строке нужно ввести: $ pip install flask

Теперь можно импортировать модуль и написать простое приложение.

Создадим файл mock.py

import flask
from flask import request

app = flask.Flask(__name__)

if __name__ == '__main__':
    app.debug = True 
    app.run()​

app – экземпляр класса Flask – и есть наше приложение. При запуске данного файла приложение будет запущено со включенным режимом дебага. Теперь опишем, что наше приложение должно делать.

Мы хотим, чтобы по корневой ссылке приложения приходил ответ, что это за приложение. Создадим для этого простую функцию (после определения app) :

@app.route('/')
def main():
    return 'This is mock service for RTDM'

Декорактор @app.route показывает, какой именно Url обрабатывается данной функцией. Функция возвращает то сообщение, которое получит клиент, обратившись по данной ссылке. Проверим это.

Для этого запустим приложение, выполнив в командной строке: $ python mock.py. 

Произойдет запуск встроенного сервера для тестирования, который по умолчанию использует адрес  http://127.0.0.1:5000/. Если перейти по этому адресу в браузере (или отправить на него HTTP GET запрос с помощью, например, Curl), то можно будет увидеть следующее:

По умолчанию поддерживается только метод HTTP GET, то есть ли мы обратимся по этой ссылке с методом POST, то получим ошибку 405 “Method not allowed”. Для использования других методов HTTP их нужно явно прописывать в декораторе функции (см. пример ниже).

Итак, мы написали аналог “Hello world” , теперь время перейти к созданию нашего mock-сервиса.

Мы хотим, чтобы при вызове HTTP POST сервис возвращал конкретный файл, лежащий на диске, по методу HTTP GET может просто возвращать название сервиса.

import settings @app.route('/service', methods=['POST', 'GET'])
def service():
    if request.method == 'POST':
        with open(settings.RESP_DEFAULT, 'rb') as f:
            return f.read()
    else: #HTTP GET
        return 'This url is for mock service, use POST method'

Путь файла указан в переменной RESP_DEFAULT в отдельном файле settings.py.

Наш сервис готов к работе, можно попробовать вызвать его по ссылке: http://127.0.0.1:5000/service

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

Мы реализуем это следующим образом: будем брать из реквеста значение тега Id и в качестве ответа отдавать файл с таким именем. Таким образом, заготовив несколько файлов-ответов можно тестировать логику при различных ответах сервиса.

Дополним функцию service:

from xml.dom import minidom
import os
@app.route('/service', methods=['POST', 'GET'])
def service():
    if request.method == 'POST':
        data = request.get_data()
        dom = minidom.parseString(data)
        req_id = dom.getElementsByTagName('Id')[0].childNodes[0].data

        files = [f for f in os.listdir(settings.FOLDER) if req_id in f]
        try:
            file_resp = os.path.join(folder, files[0])
        except:
            file_resp = settings.RESP_DEFAULT

        with open(file_resp, 'rb') as f:
            return f.read()
    else:  # HTTP GET
        return 'This url is for mock service, use POST method'

Сервис принимает на вход запросы в формате xml, поэтому для парсинга запроса используем модуль minidom стандартной библиотеки python. FOLDER (переменная в файле settings.py) – это путь к директории, в которой должны лежать файлы с ответами, названия которых соответствуют тегу Id в реквесте. Если файл соответствующий Id не найден, то возвращается ответ по умолчанию, так же, как и раньше.

Итак, наш сервис готов к использованию по ссылке http://127.0.0.1:5000/service

Следующий этап – это деплой сервиса на подходящий веб-сервер, например, Apache, но это тема для отдельной статьи.

Приложение

Итоговое содержимое файла mock.py:

import settings
from xml.dom import minidom
import os
import flask
from flask import request

app = flask.Flask(__name__)

@app.route('/')
def main():
    return 'This is mock service for RTDM'

 

@app.route('/service', methods=['POST', 'GET'])
def service():
    if request.method == 'POST':
        data = request.get_data()
        dom = minidom.parseString(data)
        req_id = dom.getElementsByTagName('Id')[0].childNodes[0].data

        files = [f for f in os.listdir(settings.FOLDER) if req_id in f]
        try:
            file_resp = os.path.join(folder, files[0])
        except:
            file_resp = settings.RESP_DEFAULT

        with open(file_resp, 'rb') as f:
            return f.read()
    else: 
        return 'This url is for mock service, use POST method'


if __name__ == '__main__':
    app.debug = True 
    app.run()