| Crates.io | picotest |
| lib.rs | picotest |
| version | 1.8.0 |
| created_at | 2025-03-24 09:31:22.462755+00 |
| updated_at | 2025-08-13 08:22:16.005052+00 |
| description | Test framework for Picodata plugin |
| homepage | https://picodata.io/ |
| repository | https://github.com/picodata/picotest |
| max_upload_size | |
| id | 1603605 |
| size | 112,794 |
Picotest - это фреймворк для тестирования плагинов, созданных в окружении pike.
Для использования Picotest требуется выполнить следующие действия:
cargo install picodata-pike
Cargo.toml плагина:cargo add --dev picotest
cargo add --dev rstest
| Picotest | Picodata |
|---|---|
| >= 1.4.0 | 25.2.1 |
| >= 1.4.0 | 25.1.2 |
| < 1.4.0 | 25.1.1 |
Макрос #[picotest] используется для написания интеграционных тестов и может применяться как к функциям, так и к модулям.
#[picotest]При использовании макроса на модуле picotest автоматически пометит все функции модуля, названия которых начинаются с test_, как rstest-функции.
use picotest::*;
#[picotest]
mod test_mod {
fn test_foo() {
assert_eq!("foo", "foo");
}
fn test_bar() {
assert_eq!("bar", "bar");
}
}
rstestМакрос #[picotest] является оберткой над rstest, поэтому поддерживает использование:
fixture.
once
case
use picotest::picotest;
#[picotest]
mod test_mod {
#[fixture]
fn foo() -> String {
"foo".to_string()
}
#[fixture]
#[once]
fn bar() -> String {
"bar".to_string()
}
fn test_foo(foo: String) {
assert_eq!(foo, "foo".to_string());
}
fn test_bar(bar: String) {
assert_eq!(bar, "bar".to_string());
}
fn test_foo_bar(foo: String, bar: String) {
assert_ne!(foo, bar);
}
#[case(0, 0)]
#[case(1, 1)]
#[case(2, 1)]
#[case(3, 2)]
#[case(4, 3)]
fn test_fibonacci(#[case] input: u32, #[case] expected: u32) {
assert_eq!(expected, fibonacci(input))
}
#[picotest]| Attribute | Description | Default |
|---|---|---|
path |
Путь до директории, содержащей файл топологии плагина (topology.toml) | Определяется автоматически |
timeout |
Таймаут перед запуском первого теста (seconds) | 5 |
Picotest позволяет менять конфигурацию сервисов плагина во время исполнения интеграционного теста.
Изменение конфигурации происходит с помощью метода Cluster::apply_config, который доступен при использовании макроса #[picotest].
Формат конфигурации должен соответствовать маппингу YAML, заданному в plugin_config.yaml.
Пример:
Допустим конфигурация плагина с одним сервисом "router" задана следующим образом:
router:
rpc_endpoint: "/hello"
max_rpc_message_size_bytes: 1024
max_rpc_message_queue_size: 2048
Тогда интеграционный тест, изменяющий данную конфигурацию, может выглядеть следующим образом:
#[picotest]
fn test_apply_plugin() {
// 1. Assemble YAML string.
let plugin_config_yaml = r#"
router:
rpc_endpoint: "/test"
max_rpc_message_size_bytes: 128
max_rpc_message_queue_size: 32
"#;
let plugin_config: PluginConfigMap =
serde_yaml::from_str(plugin_config_yaml).unwrap();
// 2. Apply config to the running cluster instance.
cluster // implicitly created variable by picotest magic
.apply_config(plugin_config)
.expect("Failed to apply config");
// Callback Serivce::on_config_change should've been already
// called at this point.
}
Подробнее в описании метода Cluster::apply_config.
Picotest обеспечивает полную изоляцию тестовых окружений за счет автоматического управления жизненным циклом кластера:
my_pike_plugin/
├── topology.toml # Файл топологии плагина
├── src/
│ └── lib.rs # Основной код
└── tests/
├── common/ # Вспомогательные модули (не тесты)
│ └── mod.rs
├── integration_test1.rs
└── integration_test2.rs
...
...
...
Изоляция на уровне файлов:
.rs файл в tests/ компилируется как самостоятельный исполняемый модульPicotest позволяет создавать и удалять кластер без использования макроса #[picotest].
use rstest::rstest;
#[rstest]
fn test_without_picotest_macro() {
let cluster = picotest::cluster(".", 0);
assert!(cluster.path == ".");
}
Макрос #[picotest_unit] используется для написания юнит-тестов для плагинов, созданных с помощью утилиты pike.
Для работы макроса #[picotest_unit] крейт должен быть объявлен как библиотека с поддержкой динамической линковки.
В Cargo.toml пакета необходимо указать:
[lib]
crate-type = ["cdylib"]
#[picotest_unit]
fn test_my_http_query() {
let http_client = fibreq::ClientBuilder::new().build();
let http_request = http_client.get("http://example.com").unwrap();
let http_response = http_request.send().unwrap();
assert!(http_response.status() == http_types::StatusCode::Ok);
}
Тесты запускаются через интерфейс cargo test:
cargo test
#[picotest_unit] не может использоваться в модуле под #[cfg(test)].Пример неверного использования макроса:
#[cfg(test)]
mod tests {
#[picotest_unit]
fn test_declared_under_test_cfg() {}
}
Пример верного использования макроса:
mod tests {
#[picotest_unit]
fn test_is_NOT_declared_under_test_cfg() {}
}
По скольку каждый юнит-тест компилируется и линкуется в динамическую библиотеку плагина (см. Структура плагина), он не должен быть задан в конфигурации, отличной от debug. В противном случае при сборке тестов они будут проигнорированы компилятором.
#[picotest_unit] не может использоваться совместно с другими атрибутами.Все атрибуты используемые совместно с макросом будут отброшены.
В примере ниже #[should_panic] будет отброшен в процессе компиляции.
#[should_panic]
#[picotest_unit]
fn test_will_ignore_should_panic_attribute() {}
Picotest при запуске создаст дополнительного пользователя и назначит права на создание таблиц
User: Picotest
Password: Pic0test
Пример использования pgproto
use picotest::*;
use picotest_helpers::{PICOTEST_USER, PICOTEST_USER_PASSWORD};
use postgres::{Client, NoTls};
#[derive(Debug, PartialEq, Eq)]
struct User {
id: i64,
name: String,
last_name: String,
}
#[picotest]
fn test_pg_connection() {
let conn_string = format!(
"host=localhost port={} user={} password={}",
cluster.main().pg_port,
PICOTEST_USER,
PICOTEST_USER_PASSWORD
);
let mut client = Client::connect(conn_string.as_str(), NoTls).unwrap();
client
.execute(
"
CREATE TABLE IF NOT EXISTS users (
Id INT PRIMARY KEY,
Name VARCHAR(50) NOT NULL,
LastName VARCHAR(50) NOT NULL
)",
&[],
)
.unwrap();
let user = User {
id: 1,
name: "Picotest".into(),
last_name: "Picotest".into(),
};
client
.execute(
&format!(
"INSERT INTO users (Id, Name, LastName) VALUES ({}, '{}', '{}')",
user.id, user.name, user.last_name
),
&[],
)
.unwrap();
let users = client
.query("SELECT Id, Name, LastName FROM users", &[])
.unwrap()
.iter()
.map(|row| User {
id: row.get("id"),
name: row.get("Name"),
last_name: row.get("LastName"),
})
.collect::<Vec<_>>();
assert_eq!(users.len(), 1);
let pg_user = users.first().unwrap();
assert_eq!(&user, pg_user);
}
Для тестирования RPC ручек плагинов, предлагается использовать функцию PicotestInstance::execute_rpc, вызванную на конкретном инстансе, на котором задан RPC endpoint.
Аргументы функции:
/testmain0.1.0User { ... }Тип тела запроса и возвращаемого значения определяется через шаблонные параметры.
Для тестов необходим асинхронный рантайм, поэтому необходимо указать макрос #[tokio::test] и добавить к самому тесту модификатор async.
Пример теста:
#[derive(Serialize, Deserialize, Debug)]
pub struct User {
name: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ExampleResponse {
rpc_hello_response: String,
}
#[tokio::test]
#[picotest(path = "../tmp/test_plugin")]
async fn test_rpc_handle() {
let user_to_send = User {
name: "Dodo".to_string(),
};
let tnt_response = cluster
.main()
.execute_rpc::<User, ExampleResponse>(
"test_plugin",
"/greetings_rpc",
"main",
"0.1.0",
&user_to_send,
)
.await
.unwrap();
assert_eq!(
tnt_response.rpc_hello_response,
"Hello Dodo, long time no see."
);
}
Для выполнения кода из консоли администратора на первом инстансе воспользуйтесь:
#[picotest]
fn test_query() {
cluster.run_query("SELECT * FROM users"); // Выполнение SQL запроса
cluster.run_lua("box.space.users:select()"); // Выполнение LUA кода
}
Если существует необходимость выполнить скрипт на любом другом инстансе:
#[picotest]
fn test_run_query_on_instance() {
cluster.instances[1].run_query("SELECT * FROM users");
cluster.instances[2].run_lua("box.space.users:select()");
}
Метод stop_instance позволяет остановить конкретный экземпляр (instance) внутри кластера Picotest.
#[picotest]
fn test_stop_cluster_instance() {
// получаем список всех инстансов в кластере
let instances = cluster.instances();
// останавливаем выбранный инстанс
cluster.stop_instance(&instances[1])
}