[Перевод] Мокаем сервисы с Mountebank и Go

7bbfc4fa6690414a5c40dff4ba1eb813.jpg

Содержание

  • Почему именно Mountebank?  

  • Описание компонентов Mountebank«s 

  • Установка Mountebank 

  • Mountebank в действии:

    • Запускаем локальный сервис

    • Запускаем Mountebank 

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

  • Что еще?

  • Заключение 

Почему именно Mountebank?

Если кратко, Mountebank это легкий, автономный инструмент с открытым исходным кодом, который можно использовать для имитации HTTP, SMTP и TCP сервисов. Его можно сконфигурировать так, чтобы он возвращал предопределенные ответы на запросы или проксировал запросы в реальный сервис. 

Существует множество различных mock-сервисов, позволяющих заменять внешние сервисы и изолировать тот, который нам нужно протестировать. Кроме того, в моей команде мы пробовали использовать Elasticsearch и другие утилиты для логгирования, но это всё не было оптимальным решением, так как оно не было стабильным и приводило к появлению flaky-тестов, а кроме того, это дорого. Более того, если вам нужно использовать mock-сервисы, вам скорее всего придётся запускать их в отдельном шаге вашего пайплайна.  

Вообще, перед тем как мы остановили свой выбор на Mountebank, мы сравнивали два отличных инструмента: Mountebank и Wiremock. Но после некоторого исследования мы выяснили, что Mountebank имеет немного большее количество контрибьюторов, а его веб-интерфейс более интуитивный и дружелюбный, чем Java-based подход Wiremock-а. 

Mountebank это простой автономный Node.js сервер, который необязательно рестартовать на каждый запуск тестов. Его конфигурацию можно просто обновить во время запуска тестов. Это независимый сервис, который «сидит в середине» и определяет, какие запросы направлять на реальные сервисы, а на какие отвечать заглушкой (mock). Он отделен от тестового фреймворка, и всё, что нужно сделать — это изменить эндпоинты.  

Из официальной документации (линк), Mountebank имеет такие преимущества:  

  • С ним легко начать работу 
    Mountebank прост в установке и не имеет зависимостей от используемых платформ. Mountebank предоставляет увлекательную и исчерпывающую документацию с большим количеством примеров, а также с приятным пользовательским интерфейсом, который позволяет исследовать API в интерактивном режиме. 

  • Не просто инструмент, а платформа 
    Mountebank стремится быть полностью кросс-платформенным с привязкой к нативным языкам. Если функциональности «из коробки» оказывается недостаточно, то платформа предоставляет возможности расширения серверов скриптами. 

  • Мощность  
    Mountebank это единственный инструмент виртуализации с открытым исходным кодом, который является немодальным и многопротокольным. Есть платные аналоги, но их модели лицензирования затрудняют перемещение тестов ближе к разработке, и они даже могут требовать использования специализированной IDE. Mountebank же обеспечивает бесплатную виртуализацию без каких-либо ограничений платформ. 

Кроме того, моя команда по тестированию микросервисов поддчеркнула еще некоторые преимущества Mountebank:  

  • Совместим с Golang  
    Это позволяет командам, использующим Go в качестве основного языка программирования легко интегрировать Mountebank в процесс разработки. Будучи Golang совместимым, Mountebank позволяет создавать mock-серверы для приложений написанных на Go, что упрощает процесс тестирования и ускоряет разработку. 

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

  • Конфигурация на лету 
    Мы можем делать изменения на mock сервере прямо во время выполнения тестов, без рестарта его самого. Такая возможность сильно упрощает тестирование различных сценариев и граничных случаев, и позволяет улучшать тестовую стратегию на лету. Более того, конфигурация во время выполнения помогает создавать более сложные тестовые сценарии, мы можем изменять поведение mock сервера с учетом результатов выполнения предыдущих запросов, создавая более реалистичные и вариативные тесты, которые будут лучше симулировать поведение API в реальной жизни. 

Однако, Mountebank не занимается валидацией, это веб-сервис, который собирает отправленные запросы и возвращает определенные ответы на них для сконфигурированных заглушек. Поэтому, во время тестов мы не отправляем никаких данных в реальные сервисы, вместо этого мы проверяем данные в самом Mountebank 

Использование Mountebank's при тестировании

Использование Mountebank’s при тестировании

Описание компонентов Mountebank’s

Mountebank оперирует двумя основными компонентами — Imposters и Stubs

  • Imposter-ы — виртуальные сервисы, создаваемые Mountebank и симулирующие реальный сервис. Каждый Imposter может иметь несколько эндпоинтов для различных URL или портов на виртуальном сервисе. Например, если вы мокаете HTTP сервис, вы можете создать imposter с несколькими эндпоинтами, каждый из которых будет соответствовать разным URL (например, /api/v1/users, /api/v1/products, и т.д.). Imposters также поддерживают разные протоколы, включая HTTP, HTTPS, TCP, SMTP и другие. 

  • Stub-ы,  в свою очередь, определяют поведение для различных Imposter-ов. Stub-ы можно использовать для симуляции ответов от внешних сервисов или для создания общего шаблона ответа, который будет использоваться для различных эндпоинтов. Например, можно создать stub, симулирующий успешный ответ от внешнего сервиса аутентификации, и затем использовать этот  stub со всеми imposter-ами, требующими аутентификации. Плюс использования stub-ов таким образом в том, что они могут сократить время и усилия на создание mock сервисов, так как позволяют переиспользовать часто используемые ответы на многих эндпоинтах. Это позволяет упростить создание и поддержку mock сервисов, особенно для больших и более сложных приложений. 

Говоря проще, imposter определяют поведение вашего mocked сервиса, и содержат один или больше stub-ов. Stub же определяет эндпоинты для вашего mocked сервиса и то, как обрабатывать каждый запрос. 

Установка Mountebank

Mountebank это Node.js приложение. Для его установки просто запустите команду npm install -g mountebank в вашем терминале. Также можно проверить установку с помощью команды mb --version. Она должна показать версию установленного Mountebank. 

И это всё! Теперь ваш терминал может работать с командой mb, которая запускает Mountebank локально. 

Mountebank в действии

В этой статье мы разберем, как на реальном примере работает Mountebank. Мы будем использовать очень простой сервис «transform», который я подготовил для примера. Что делает этот сервис:  

  1. Получает данные. 

  1. Обновляет в них определенные поля. 

  1. Отправляет обновленные данные далее во внешний сервис. 

  • Цель демо 
    Протестировать сервис в изоляции, с возможностью провалидировать исходящие данные во внешний сервис. 

  • Стратегия для достижения цели 
    Использовать Mountebank чтобы отловить и провалидировать данные. Для этого мы изменим эндпоинт внешнего сервиса так, чтобы наш тестируемый сервис обращался к Mountebank URL. 

Использование Mountebank в данном примере 

Использование Mountebank в данном примере 

Запускаем локальный сервис

Как я упоминал, в этой статье мы будем использовать некий »transform» сервис,  его вы можете найти ниже. 

Запускаем go run main.go в папке вашего сервиса чтобы запустить его локально. Когда вы увидите [GIN-debug] Listening and serving HTTP on localhost:8080, ваш локальный сервис запущен и готов к преобразованию запросов. 

// 'payload' represents data accepted by transform function
type payload struct {
	Key1 string `json:"Key1"`
	Key2 string `json:"Key2"`
}

var mountebankUrl = "http://localhost:8181/transform"
var httpClient = &http.Client{}

func main() {
	router := gin.Default()
	router.POST("/transform", transform)
	router.Run("localhost:8080")
}

// 'transform' function updates received payload and sends it further
func transform(receivedRequest *gin.Context) {
	var payload payload
	_ = receivedRequest.BindJSON(&payload)

	// Transforming the payload
	payload.Key1 = "transformed value 1"
	payload.Key2 = "transformed value 2"

	// Converting transformed payload to JSON
	jsonPayload, _ := json.Marshal(payload)

	// Sending a request to external service
	request, _ := http.NewRequest("POST", mountebankUrl, bytes.NewBuffer(jsonPayload))
	request.Header.Add("Testing-Id", receivedRequest.GetHeader("Testing-Id"))
	httpClient.Do(request)
}

Запускаем Mountebank

Выполняем команду mb в терминале для запуска Mountebank. 

Если вы видите »info: [mb:2525] mountebank v2.8.1 now taking orders — point your browser to http://localhost:2525/ for help» —  значит Mountebank локально запустился, можно в этом убедиться открыв ссылку http://localhost:2525 

Веб интерфейс Mountebank

Веб интерфейс Mountebank

Поскольку работа Mountebank построена вокруг imposter-ов, которых у нас пока нет, давайте сконфигурируем один:  

var Imposter = mbgo.Imposter{
	Port:           8181,
	Proto:          "http",
	Name:           "imposter",
	RecordRequests: true,
	AllowCORS:      true,
	DefaultResponse: mbgo.HTTPResponse{
		StatusCode: 200,
	},
	Stubs: []mbgo.Stub{},
}

Как видите из кода выше, imposter также должен содержать stub, определим его:  

var TransformStub = mbgo.Stub{
	Predicates: []mbgo.Predicate{
		{
			Operator: "equals",
			Request: &mbgo.HTTPRequest{
				Method: "POST",
				Path:   "/transform",
			},
		},
	},
	Responses: []mbgo.Response{
		{
			Type: "is",
			Value: mbgo.HTTPResponse{
				StatusCode: 200,
			},
		},
	},
}

В принципе, это всё что нужно сделать для конфигурации Mountebank. Сконфигурированные imposter и stub появятся в Mountebank когда вы запустите тесты. Теперь предлагаю добавить некоторые дополнительные функции в mountebank.go файл, чтобы немного упростить тестирование:  

type Mountebank struct {
	client *mbgo.Client
}

func NewMountebank() (*Mountebank, error) {
	httpClient := &http.Client{}
	mbURL, err := url.Parse("http://localhost:2525/")
	if err != nil {
		return &Mountebank{}, err
	}
	client := mbgo.NewClient(httpClient, mbURL)

	return &Mountebank{client: client}, nil
}

func (m Mountebank) CreateImposter(ctx context.Context, imposter mbgo.Imposter) error {
	_, err := m.client.Imposter(ctx, imposter.Port, false)
	if err != nil {
		_, err = m.client.Create(ctx, imposter)
		if err != nil {
			return err
		}
	}

	return nil
}

func (m Mountebank) AddStub(ctx context.Context, port int, newStub mbgo.Stub) error {
	_, err := m.client.Imposter(ctx, port, true)
	if err != nil {
		return err
	}
	_, err = m.client.AddStub(ctx, port, -1, newStub)

	return err
}

func (m Mountebank) GetRequestsFromMountebank(ctx context.Context, port int) ([]mbgo.HTTPRequest, error) {
	res, err := m.client.Imposter(ctx, port, false)
	if err != nil {
		return nil, err
	}
	var requests []mbgo.HTTPRequest
	for _, request := range res.Requests {
		r := request.(*mbgo.HTTPRequest)
		requests = append(requests, *r)
	}

	return requests, nil
}

Мы добавили некоторые полезные функции в упомянутый mountebank.go файл, они будут использоваться для:  

  • Инициализации новой переменной Mountebank. 

  • Создания нового Imposter. 

  • Добавления Stub-ов к Imposter-у. 

  • Получения всех запросов, хранящихся в Mountebank. 

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

Теперь мы добавим тест в папку /service. Тест будет:  

  1. Добавлять imposter и stub в Mountebank. 

  2. Отправлять запрос в «transform» сервис.

  3. Получать запрос из Mountebank используя кастомный заголовок testingID

var (
	ctx = context.Background()
)

func TestTransform(t *testing.T) {

	// Initializing Mountebank's components
	mbank, err := mountebank.NewMountebank()
	if err != nil {
		t.Fatalf("Error: %v", err)
	}
	err = mbank.CreateImposter(ctx, mountebank.Imposter)
	if err != nil {
		t.Fatalf("Error: %v", err)
	}
	err = mbank.AddStub(ctx, mountebank.Imposter.Port, stubs.TransformStub)
	if err != nil {
		t.Fatalf("Error: %v", err)
	}

	// Creating a testing data
	payload1 := "{\"Key1\": \"initial value 1\", \"Key2\": \"initial value 2\"}"

	// Sending the request to 'transform' service
	request, _ := http.NewRequest("POST", "http://localhost:8080/transform", bytes.NewBuffer([]byte(payload1)))
	request.Header.Add("Testing-Id", "1001")
	httpClient.Do(request)
	log.Printf("Payload sent to local service:\n%s", payload1)

	// Retrieving all requests present in Mountebank
	reqs, _ := mbank.GetRequestsFromMountebank(ctx, 8181)

	// Looping through Mountebank requests to find the one with knows Testing ID
	for _, req := range reqs {
		if req.Headers.Get("Testing-Id") == "1001" {
			log.Printf("Request from Mountebank:\n%s", req.Body.(string))
			break
		}
	}
}

Запускаем go тест в папке /service:  

Лог теста

Лог теста

  • В логе мы видим, что наш запрос был отправлен в »transform» service:  
    {«Key1»: «initial value 1», «Key2»: «initial value 2»} 

  • Запрос также был успешно получен из Mountebank:  
    {«Key1»: «transformed value 1», «Key2»: «transformed value 2»} 

  • PASS, что означает, что наш запрос был успешно преобразован и получен из Mountebank с помощью кастомного залоговка testingID

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

Давайте также проверим страницу локального Mountebank:  http://localhost:2525/imposters/8181 

Страница Imposter с полученным запросом и созданным Stub

Страница Imposter с полученным запросом и созданным Stub

Как видим, Mountebank успешно создал stub и с его помощью получил запрос. 

Когда запрос записан Mountebank-ом, он обычно включает такую информацию:  

  • Метод 
    HTTP метод, используемый в запросе, например GET,  POST,  PUT,  DELETE и другие. 

  • URL 
    Запрошенный URL включающий query или path параметры. 

  • Заголовки (headers) 
    Заголовки, включенные в запрос, например Accept,  Content-Type,  User-Agent и другие. Они могут содержать дополнительную информацию о запросе, к примеру формат тела запроса, или версию клиента, выполнившего запрос. 

  • Тело запроса (body) 
    Тело запроса, если оно было. Это может быть JSON,  XML или любой другой формат, в зависимости от заголовка content-type. 

Дополнительно, Mountebank может отлавливать другие метаданные о запросе, такие как временная метка получения запроса, IP адрес клиента или любую другую информацию, которую вам нужно отловить. 

Сохраненные данные запроса могут быть использованы в различных сценариях. Например, их можно применить для создания mock ответов, адаптированных под конкретные запросы, или для проверки того, что запрос был получен и обработан правильно. Способность Mountebank«s собирать данные запросов и манипулировать ими — одна из ключевых его особенностей, позволяющих вам создавать весьма детально настроенный mock-сервисы для тестирования и разработки. 

Что ещё?

Есть еще и другие возможности улучшения ваших imposter-ов и stub-ов:  

  • Динамические ответы 
    Вы можете изменять ответ, основываясь на запросе, или генерировать абсолютно новый ответ. Таким образом создавая сложные и гибкие ответы, адаптированные под ваши конкретные нужды. 

  • Использование шаблонов 
    Mountebank поддерживает шаблоны ответов, что позволяет, например, добавлять различные переменные в ответ, основываясь на запросе. Это особенно полезно, если у вас есть неоходимость генерировать динамические ответы, зависящие от параметров запроса. 

  • Проксирование 
    С помощью Mountebank вы можете создавать прокси, перенаправляющие запросы на реальные веб-сервисы, и получать таким образом ответы от них. Это может быть полезно, если вы хотите протестировать API, недоступный на вашем локальном окружении. 

  • Условия 
    Mountebank также поддерживает условную логику, что позволяет определять различные варианты поведения, основываясь на запросе или ответе. Вы можете использовать этот функционал, чтобы кастомизировать поведение ваших imposters and stubs. 

  • Динамические порты 
    По умолчанию, Mountebank при запуске привязан к специфическому порту. Однако, вы можете настроить Mountebank так, чтобы он использовал динамический порт, который может быть определен прямо во время выполнения. Эта функциональность может быть очень полезной, если у вас есть необходимость использовать несколько экземпляров Mountebank на одной машине. 

Заключение

Я надеюсь, что с помощью этой статьи вы найдете Mountebank полезным для себя, ведь это мощная платформа для тестирования API и mocking-а, предоставляющая пользователям функционал и инструменты для создания гибких, настраиваемых и надежных тестовых окружений. Хотя его основной функционал связан с mocking, Mountebank идет дальше и является многофункциональной платформой, поддерживающей динамические ответы, шаблоны ответов, проксирование, логику условий и динамические порты. Mountebank это независимая платформа, избавляющая вас от необходимости запускать его каждый раз перед запуском каждого теста, таким образом экономя время и ресурсы при обеспечении надежных и стабильных результатов. Mountebank это must-have инструмент, оптимизирующий процесс тестирования API и помогающий вам разрабатывать и поставлять приложения высокого уровня качества. 

Вот репозиторий, в котором вы можете найти все использованные примеры

© Habrahabr.ru