集成测试(Integration Tests)
集成测试用于测试库的公共 API,验证不同模块之间的交互。集成测试位于 tests 目录中,每个文件都是一个独立的 crate。
基本集成测试
项目结构
my_project/
├── Cargo.toml
├── src/
│ ├── lib.rs
│ └── utils.rs
└── tests/
├── integration_test.rs
├── common/
│ └── mod.rs
└── api_tests.rs
创建集成测试
// src/lib.rs
pub mod utils;
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub struct Calculator {
value: i32,
}
impl Calculator {
pub fn new() -> Self {
Calculator { value: 0 }
}
pub fn add(&mut self, n: i32) -> &mut Self {
self.value += n;
self
}
pub fn multiply(&mut self, n: i32) -> &mut Self {
self.value *= n;
self
}
pub fn result(&self) -> i32 {
self.value
}
pub fn reset(&mut self) -> &mut Self {
self.value = 0;
self
}
}
// src/utils.rs
pub fn format_number(n: i32) -> String {
if n >= 1000 {
format!("{:.1}k", n as f64 / 1000.0)
} else {
n.to_string()
}
}
pub fn is_even(n: i32) -> bool {
n % 2 == 0
}
pub fn factorial(n: u32) -> u64 {
match n {
0 | 1 => 1,
_ => n as u64 * factorial(n - 1),
}
}
// tests/integration_test.rs
use my_project::{add, multiply, Calculator};
#[test]
fn test_basic_functions() {
assert_eq!(add(2, 3), 5);
assert_eq!(multiply(4, 5), 20);
}
#[test]
fn test_calculator_chain() {
let mut calc = Calculator::new();
let result = calc
.add(10)
.multiply(2)
.add(5)
.result();
assert_eq!(result, 25);
}
#[test]
fn test_calculator_reset() {
let mut calc = Calculator::new();
calc.add(100);
assert_eq!(calc.result(), 100);
calc.reset();
assert_eq!(calc.result(), 0);
}
#[test]
fn test_calculator_multiple_operations() {
let mut calc1 = Calculator::new();
let mut calc2 = Calculator::new();
calc1.add(10).multiply(2);
calc2.add(5).multiply(4);
assert_eq!(calc1.result(), 20);
assert_eq!(calc2.result(), 20);
}
测试工具模块
共享测试代码
// tests/common/mod.rs
use my_project::Calculator;
pub fn setup_calculator_with_value(value: i32) -> Calculator {
let mut calc = Calculator::new();
calc.add(value);
calc
}
pub fn assert_calculator_result(calc: &Calculator, expected: i32) {
assert_eq!(calc.result(), expected,
"计算器结果不匹配:期望 {},实际 {}",
expected, calc.result());
}
pub struct TestData {
pub input: i32,
pub expected: i32,
}
impl TestData {
pub fn new(input: i32, expected: i32) -> Self {
TestData { input, expected }
}
}
pub fn get_test_cases() -> Vec<TestData> {
vec![
TestData::new(0, 0),
TestData::new(1, 1),
TestData::new(5, 25),
TestData::new(-3, 9),
TestData::new(10, 100),
]
}
// tests/api_tests.rs
mod common;
use my_project::{utils, Calculator};
use common::{setup_calculator_with_value, assert_calculator_result, get_test_cases};
#[test]
fn test_utils_format_number() {
assert_eq!(utils::format_number(500), "500");
assert_eq!(utils::format_number(1500), "1.5k");
assert_eq!(utils::format_number(2000), "2.0k");
}
#[test]
fn test_utils_is_even() {
assert!(utils::is_even(2));
assert!(utils::is_even(0));
assert!(utils::is_even(-4));
assert!(!utils::is_even(1));
assert!(!utils::is_even(-3));
}
#[test]
fn test_utils_factorial() {
assert_eq!(utils::factorial(0), 1);
assert_eq!(utils::factorial(1), 1);
assert_eq!(utils::factorial(5), 120);
assert_eq!(utils::factorial(10), 3628800);
}
#[test]
fn test_calculator_with_common_setup() {
let calc = setup_calculator_with_value(42);
assert_calculator_result(&calc, 42);
}
#[test]
fn test_multiple_calculators() {
let test_cases = get_test_cases();
for case in test_cases {
let mut calc = Calculator::new();
calc.add(case.input).multiply(case.input);
assert_calculator_result(&calc, case.expected);
}
}
文件 I/O 集成测试
测试文件操作
// src/lib.rs 添加文件操作功能
use std::fs;
use std::io::{self, Write};
use std::path::Path;
pub fn save_data(filename: &str, data: &[i32]) -> io::Result<()> {
let content = data.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join(",");
fs::write(filename, content)
}
pub fn load_data(filename: &str) -> io::Result<Vec<i32>> {
let content = fs::read_to_string(filename)?;
let numbers: Result<Vec<i32>, _> = content
.split(',')
.filter(|s| !s.is_empty())
.map(|s| s.trim().parse())
.collect();
numbers.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn append_data(filename: &str, data: &[i32]) -> io::Result<()> {
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(filename)?;
for &num in data {
writeln!(file, "{}", num)?;
}
Ok(())
}
// tests/file_operations.rs
use std::fs;
use std::path::Path;
use my_project::{save_data, load_data, append_data};
struct TestFile {
path: String,
}
impl TestFile {
fn new(name: &str) -> Self {
TestFile {
path: format!("test_{}", name),
}
}
}
impl Drop for TestFile {
fn drop(&mut self) {
if Path::new(&self.path).exists() {
let _ = fs::remove_file(&self.path);
}
}
}
#[test]
fn test_save_and_load_data() {
let test_file = TestFile::new("save_load.txt");
let original_data = vec![1, 2, 3, 4, 5];
// 保存数据
save_data(&test_file.path, &original_data).unwrap();
// 验证文件存在
assert!(Path::new(&test_file.path).exists());
// 加载数据
let loaded_data = load_data(&test_file.path).unwrap();
// 验证数据一致性
assert_eq!(loaded_data, original_data);
}
#[test]
fn test_load_nonexistent_file() {
let result = load_data("nonexistent_file.txt");
assert!(result.is_err());
}
#[test]
fn test_save_empty_data() {
let test_file = TestFile::new("empty.txt");
let empty_data: Vec<i32> = vec![];
save_data(&test_file.path, &empty_data).unwrap();
let loaded_data = load_data(&test_file.path).unwrap();
assert_eq!(loaded_data, empty_data);
}
#[test]
fn test_append_data() {
let test_file = TestFile::new("append.txt");
// 创建初始文件
fs::write(&test_file.path, "").unwrap();
// 追加数据
append_data(&test_file.path, &[1, 2, 3]).unwrap();
append_data(&test_file.path, &[4, 5]).unwrap();
// 读取并验证
let content = fs::read_to_string(&test_file.path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines, vec!["1", "2", "3", "4", "5"]);
}
#[test]
fn test_large_data_set() {
let test_file = TestFile::new("large.txt");
let large_data: Vec<i32> = (0..10000).collect();
save_data(&test_file.path, &large_data).unwrap();
let loaded_data = load_data(&test_file.path).unwrap();
assert_eq!(loaded_data.len(), 10000);
assert_eq!(loaded_data, large_data);
}
HTTP 客户端集成测试
模拟 HTTP 服务
// Cargo.toml 添加依赖
// [dev-dependencies]
// mockito = "1.0"
// tokio = { version = "1.0", features = ["full"] }
// src/lib.rs 添加 HTTP 客户端
use std::collections::HashMap;
pub struct HttpClient {
base_url: String,
}
impl HttpClient {
pub fn new(base_url: String) -> Self {
HttpClient { base_url }
}
pub async fn get(&self, path: &str) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}{}", self.base_url, path);
let response = reqwest::get(&url).await?;
let text = response.text().await?;
Ok(text)
}
pub async fn post(&self, path: &str, data: &HashMap<String, String>)
-> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}{}", self.base_url, path);
let client = reqwest::Client::new();
let response = client.post(&url).json(data).send().await?;
let text = response.text().await?;
Ok(text)
}
}
// tests/http_client_tests.rs
use std::collections::HashMap;
use my_project::HttpClient;
use mockito::{mock, server_url};
#[tokio::test]
async fn test_get_request() {
let _m = mock("GET", "/api/users")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"users": ["Alice", "Bob"]}"#)
.create();
let client = HttpClient::new(server_url());
let response = client.get("/api/users").await.unwrap();
assert!(response.contains("Alice"));
assert!(response.contains("Bob"));
}
#[tokio::test]
async fn test_post_request() {
let _m = mock("POST", "/api/users")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{"id": 123, "name": "Charlie"}"#)
.create();
let client = HttpClient::new(server_url());
let mut data = HashMap::new();
data.insert("name".to_string(), "Charlie".to_string());
let response = client.post("/api/users", &data).await.unwrap();
assert!(response.contains("Charlie"));
assert!(response.contains("123"));
}
#[tokio::test]
async fn test_error_handling() {
let _m = mock("GET", "/api/error")
.with_status(500)
.create();
let client = HttpClient::new(server_url());
let result = client.get("/api/error").await;
// 根据具体的错误处理逻辑进行断言
assert!(result.is_err() || result.unwrap().is_empty());
}
数据库集成测试
内存数据库测试
// src/lib.rs 添加数据库操作
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
}
pub struct UserRepository {
users: HashMap<u32, User>,
next_id: u32,
}
impl UserRepository {
pub fn new() -> Self {
UserRepository {
users: HashMap::new(),
next_id: 1,
}
}
pub fn create_user(&mut self, name: String, email: String) -> User {
let user = User {
id: self.next_id,
name,
email,
};
self.users.insert(self.next_id, user.clone());
self.next_id += 1;
user
}
pub fn get_user(&self, id: u32) -> Option<&User> {
self.users.get(&id)
}
pub fn update_user(&mut self, id: u32, name: Option<String>, email: Option<String>) -> Option<User> {
if let Some(user) = self.users.get_mut(&id) {
if let Some(new_name) = name {
user.name = new_name;
}
if let Some(new_email) = email {
user.email = new_email;
}
Some(user.clone())
} else {
None
}
}
pub fn delete_user(&mut self, id: u32) -> bool {
self.users.remove(&id).is_some()
}
pub fn list_users(&self) -> Vec<&User> {
self.users.values().collect()
}
}
// tests/database_tests.rs
use my_project::{User, UserRepository};
#[test]
fn test_create_user() {
let mut repo = UserRepository::new();
let user = repo.create_user("Alice".to_string(), "alice@example.com".to_string());
assert_eq!(user.id, 1);
assert_eq!(user.name, "Alice");
assert_eq!(user.email, "alice@example.com");
}
#[test]
fn test_get_user() {
let mut repo = UserRepository::new();
let created_user = repo.create_user("Bob".to_string(), "bob@example.com".to_string());
let retrieved_user = repo.get_user(created_user.id);
assert!(retrieved_user.is_some());
assert_eq!(retrieved_user.unwrap(), &created_user);
}
#[test]
fn test_get_nonexistent_user() {
let repo = UserRepository::new();
let user = repo.get_user(999);
assert!(user.is_none());
}
#[test]
fn test_update_user() {
let mut repo = UserRepository::new();
let user = repo.create_user("Charlie".to_string(), "charlie@example.com".to_string());
let updated_user = repo.update_user(
user.id,
Some("Charles".to_string()),
Some("charles@example.com".to_string())
);
assert!(updated_user.is_some());
let updated_user = updated_user.unwrap();
assert_eq!(updated_user.name, "Charles");
assert_eq!(updated_user.email, "charles@example.com");
// 验证更新后的用户
let retrieved_user = repo.get_user(user.id).unwrap();
assert_eq!(retrieved_user.name, "Charles");
}
#[test]
fn test_delete_user() {
let mut repo = UserRepository::new();
let user = repo.create_user("Diana".to_string(), "diana@example.com".to_string());
let deleted = repo.delete_user(user.id);
assert!(deleted);
let retrieved_user = repo.get_user(user.id);
assert!(retrieved_user.is_none());
}
#[test]
fn test_list_users() {
let mut repo = UserRepository::new();
repo.create_user("User1".to_string(), "user1@example.com".to_string());
repo.create_user("User2".to_string(), "user2@example.com".to_string());
repo.create_user("User3".to_string(), "user3@example.com".to_string());
let users = repo.list_users();
assert_eq!(users.len(), 3);
let names: Vec<&String> = users.iter().map(|u| &u.name).collect();
assert!(names.contains(&&"User1".to_string()));
assert!(names.contains(&&"User2".to_string()));
assert!(names.contains(&&"User3".to_string()));
}
#[test]
fn test_multiple_operations() {
let mut repo = UserRepository::new();
// 创建用户
let user1 = repo.create_user("Alice".to_string(), "alice@example.com".to_string());
let user2 = repo.create_user("Bob".to_string(), "bob@example.com".to_string());
// 更新用户
repo.update_user(user1.id, Some("Alicia".to_string()), None);
// 删除用户
repo.delete_user(user2.id);
// 验证最终状态
let users = repo.list_users();
assert_eq!(users.len(), 1);
assert_eq!(users[0].name, "Alicia");
assert_eq!(users[0].email, "alice@example.com");
}
运行集成测试
测试命令
# 运行所有集成测试
cargo test --test integration_test
# 运行特定集成测试文件
cargo test --test api_tests
# 运行所有测试(单元测试 + 集成测试)
cargo test
# 只运行集成测试
cargo test --tests
# 运行特定的集成测试函数
cargo test --test integration_test test_calculator_chain
# 显示集成测试输出
cargo test --test integration_test -- --nocapture
测试配置
# Cargo.toml
[dev-dependencies]
mockito = "1.0"
tokio = { version = "1.0", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
tempfile = "3.0"
集成测试确保你的库的公共 API 按预期工作,并且不同组件能够正确协作。下一节我们将学习文档测试。