wiremock-rs: HTTP Mock-сервер для Rust
В современной разработке программного обеспечения тестирование является неотъемлемой частью процесса. Особенно сложно бывает тестировать код, который взаимодействует с внешними HTTP-сервисами. Здесь на помощь приходит библиотека wiremock-rs — мощный инструмент для создания HTTP mock-серверов в экосистеме Rust.
wiremock-rs предоставляет возможность эмулировать поведение внешних HTTP-сервисов, позволяя разработчикам тестировать свой код в изолированной среде. Вместо того чтобы отправлять реальные запросы к внешним API (что может быть медленно, ненадежно или дорого), вы можете создать локальный mock-сервер, который будет отвечать точно так, как вы зададите. Это особенно полезно при написании интеграционных тестов, тестировании обработки ошибок и разработке клиентских библиотек.
Библиотека вдохновлена популярным Java-инструментом WireMock, но полностью переписана на Rust с учетом идиом и особенностей языка. wiremock-rs поддерживает все основные возможности: сопоставление запросов по URL, заголовкам, телу; возврат различных HTTP-статусов; задержки ответов; проверку количества вызовов и многое другое.
Установка
Для установки wiremock-rs добавьте зависимость в ваш файл Cargo.toml:
[dev-dependencies]
wiremock = "0.6"
Затем выполните команду:
cargo build
Также убедитесь, что у вас установлен Rust и Cargo. Если нет — установите через rustup.
Быстрый старт
Давайте создадим простейший mock-сервер, который отвечает на GET-запросы:
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path};
#[tokio::main]
async fn main() {
// Запускаем mock-сервер на случайном порту
let mock_server = MockServer::start().await;
// Создаем правило: на GET /hello отвечаем статусом 200 и телом "Hello, World!"
let mock = Mock::given(method("GET"))
.and(path("/hello"))
.respond_with(ResponseTemplate::new(200)
.set_body_string("Hello, World!"));
// Регистрируем правило на сервере
mock_server.register(mock).await;
// Теперь можно делать запросы к серверу
let client = reqwest::Client::new();
let response = client.get(&format!("{}/hello", &mock_server.uri()))
.send()
.await
.unwrap();
println!("Статус: {}", response.status());
println!("Тело: {}", response.text().await.unwrap());
// Останавливаем сервер (произойдет автоматически при завершении main)
}
Этот минимальный пример показывает основную идею: вы запускаете сервер, определяете правила (matchers) и ответы (response templates), а затем регистрируете их. После этого ваш клиентский код может делать запросы к mock-серверу.
Основные методы и функции
1. MockServer::start()
Сигнатура: pub async fn start() -> MockServer
Описание: Запускает новый mock-сервер на случайном доступном порту. Сервер будет работать до тех пор, пока объект MockServer не будет удален.
let server = MockServer::start().await;
println!("Сервер запущен на {}", server.uri());
2. MockServer::start_on_port(port: u16)
Сигнатура: pub async fn start_on_port(port: u16) -> MockServer
Описание: Запускает mock-сервер на указанном порту. Полезно, когда нужен фиксированный порт для конфигурации.
let server = MockServer::start_on_port(8080).await;
assert_eq!(server.uri(), "http://127.0.0.1:8080");
3. Mock::given(matcher: impl Matcher)
Сигнатура: pub fn given(matcher: impl Matcher) -> MockBuilder
Описание: Создает новое правило для mock-сервера. Принимает первый matcher, который определяет условие для запроса.
let mock = Mock::given(method("POST"))
.and(path("/api/users"))
.respond_with(ResponseTemplate::new(201));
4. Mock::and(matcher: impl Matcher)
Сигнатура: pub fn and(self, matcher: impl Matcher) -> MockBuilder
Описание: Добавляет дополнительное условие к правилу. Все условия должны быть выполнены для срабатывания правила.
Mock::given(method("GET"))
.and(path("/api/data"))
.and(header("Authorization", "Bearer token123"))
.respond_with(ResponseTemplate::new(200));
5. ResponseTemplate::new(status: u16)
Сигнатура: pub fn new(status: u16) -> ResponseTemplate
Описание: Создает шаблон ответа с указанным HTTP-статусом. Это базовый строительный блок для определения ответов.
let response = ResponseTemplate::new(404)
.set_body_string("Not Found")
.set_delay(Duration::from_millis(100));
6. ResponseTemplate::set_body_string(body: impl Into<String>)
Сигнатура: pub fn set_body_string(self, body: impl Into<String>) -> ResponseTemplate
Описание: Устанавливает тело ответа в виде строки. Можно использовать как обычные строки, так и сериализованные JSON-данные.
let json_body = serde_json::json!({
"id": 1,
"name": "Alice"
}).to_string();
ResponseTemplate::new(200)
.set_body_string(json_body);
7. MockServer::register(mock: Mock)
Сигнатура: pub async fn register(&self, mock: Mock)
Описание: Регистрирует правило на сервере. После этого сервер начнет обрабатывать запросы, соответствующие этому правилу.
let mock = Mock::given(method("DELETE"))
.and(path("/api/items/1"))
.respond_with(ResponseTemplate::new(204));
server.register(mock).await;
8. MockServer::verify(&self, mock: &MockAssertion)
Сигнатура: pub async fn verify(&self, mock: &MockAssertion) -> Result<(), Vec<VerificationError>>
Описание: Проверяет, что определенное правило было вызвано указанное количество раз. Полезно для верификации поведения в тестах.
// Создаем правило
let mock = Mock::given(method("GET"))
.and(path("/api/status"))
.respond_with(ResponseTemplate::new(200))
.expect(2); // Ожидаем 2 вызова
server.register(mock).await;
// ... выполняем запросы ...
// Проверяем, что правило было вызвано ровно 2 раза
server.verify(&mock).await.unwrap();
9. MockBuilder::with_header(name: &str, value: &str)
Сигнатура: pub fn with_header(self, name: &str, value: &str) -> MockBuilder
Описание: Добавляет заголовок к ответу. Можно добавлять несколько заголовков.
Mock::given(method("GET"))
.and(path("/api/data"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("{}")
.with_header("Content-Type", "application/json")
.with_header("X-Custom-Header", "value")
);
10. ResponseTemplate::set_delay(delay: Duration)
Сигнатура: pub fn set_delay(self, delay: Duration) -> ResponseTemplate
Описание: Добавляет задержку перед отправкой ответа. Полезно для тестирования таймаутов и поведения при медленных ответах.
use std::time::Duration;
ResponseTemplate::new(200)
.set_body_string("Slow response")
.set_delay(Duration::from_secs(5)); // Задержка 5 секунд
Реальный практический пример
Рассмотрим более сложный пример: тестирование клиента для работы с API погоды. Мы создадим mock-сервер, который эмулирует работу реального сервиса погоды, включая обработку ошибок и различные сценарии.
use wiremock::{MockServer, Mock, ResponseTemplate};
use wiremock::matchers::{method, path, query_param};
use serde::{Deserialize, Serialize};
use std::time::Duration;
// Структуры данных
#[derive(Debug, Serialize, Deserialize)]
struct WeatherResponse {
city: String,
temperature: f64,
condition: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct ErrorResponse {
error: String,
code: u16,
}
// Клиент для работы с API погоды
struct WeatherClient {
base_url: String,
client: reqwest::Client,
}
impl WeatherClient {
fn new(base_url: &str) -> Self {
Self {
base_url: base_url.to_string(),
client: reqwest::Client::new(),
}
}
async fn get_weather(&self, city: &str) -> Result {
let url = format!("{}/weather?city={}", self.base_url, city);
let response = self.client.get(&url).send().await.map_err(|e| e.to_string())?;
if response.status().is_success() {
response.json::().await.map_err(|e| e.to_string())
} else {
let error = response.json::().await.map_err(|e| e.to_string())?;
Err(format!("{}: {}", error.error, error.code))
}
}
async fn get_weather_with_timeout(&self, city: &str, timeout: Duration) -> Result {
let url = format!("{}/weather?city={}", self.base_url, city);
let response = tokio::time::timeout(timeout, self.client.get(&url).send())
.await
.map_err(|_| "Request timed out".to_string())?
.map_err(|e| e.to_string())?;
if response.status().is_success() {
response.json::().await.map_err(|e| e.to_string())
} else {
let error = response.json::().await.map_err(|e| e.to_string())?;
Err(format!("{}: {}", error.error, error.code))
}
}
}
#[tokio::main]
async fn main() {
// Запускаем mock-сервер
let mock_server = MockServer::start().await;
let client = WeatherClient::new(&mock_server.uri());
// Тест 1: Успешный ответ
println!("=== Тест 1: Успешный ответ ===");
let success_mock = Mock::given(method("GET"))
.and(path("/weather"))
.and(query_param("city", "Moscow"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(serde_json::to_string(&WeatherResponse {
city: "Moscow".to_string(),
temperature: 22.5,
condition: "Sunny".to_string(),
}).unwrap())
.with_header("Content-Type", "application/json")
);
mock_server.register(success_mock).await;
match client.get_weather("Moscow").await {
Ok(weather) => println!("Погода в {}: {}°C, {}", weather.city, weather.temperature, weather.condition),
Err(e) => println!("Ошибка: {}", e),
}
// Тест 2: Ошибка 404 - город не найден
println!("\n=== Тест 2: Город не найден ===");
let not_found_mock = Mock::given(method("GET"))
.and(path("/weather"))
.and(query_param("city", "UnknownCity"))
.respond_with(
ResponseTemplate::new(404)
.set_body_string(serde_json::to_string(&ErrorResponse {
error: "City not found".to_string(),
code: 404,
}).unwrap())
.with_header("Content-Type", "application/json")
);
mock_server.register(not_found_mock).await;
match client.get_weather("UnknownCity").await {
Ok(weather) => println!("Погода: {:?}", weather),
Err(e) => println!("Ошибка: {}", e),
}
// Тест 3: Таймаут (медленный ответ)
println!("\n=== Тест 3: Таймаут ===");
let slow_mock = Mock::given(method("GET"))
.and(path("/weather"))
.and(query_param("city", "SlowCity"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string("{}")
.set_delay(Duration::from_secs(10)) // Задержка 10 секунд
);
mock_server.register(slow_mock).await;
match client.get_weather_with_timeout("SlowCity", Duration::from_secs(3)).await {
Ok(weather) => println!("Погода: {:?}", weather),
Err(e) => println!("Ошибка: {}", e),
}
// Тест 4: Проверка количества вызовов
println!("\n=== Тест 4: Верификация вызовов ===");
let verify_mock = Mock::given(method("GET"))
.and(path("/weather"))
.and(query_param("city", "Verified"))
.respond_with(ResponseTemplate::new(200).set_body_string("{}"))
.expect(1); // Ожидаем ровно 1 вызов
mock_server.register(verify_mock).await;
// Делаем запрос
let _ = client.get_weather("Verified").await;
// Проверяем, что было ровно 1 обращение
match mock_server.verify(&verify_mock).await {
Ok(_) => println!("Верификация пройдена: ровно 1 вызов"),
Err(errors) => println!("Ошибка верификации: {:?}", errors),
}
println!("\nВсе тесты завершены!");
}