我正在阅读《从零到生产》一书,该书的第 74-76 页讨论了测试函数在运行多次时如何失败,因为每次我们运行
cargo test
测试函数都会运行应用程序并且有一个插入查询在应用程序中,因此每次我们运行测试时,都会调用应用程序并执行插入查询,并插入相同的值,这会由于唯一键约束而导致错误。这本书提供了一个解决方案://! tests/health_check.rs
use std::net::TcpListener;
use sqlx::{PgConnection, PgPool, Connection, Executor};
use zero2prod::configuration::{get_configuration, DatabaseSettings};
use zero2prod::startup::run;
use uuid::Uuid;
pub struct TestApp {
pub address: String,
pub pool: PgPool,
}
async fn spawn_app() -> TestApp {
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind to the port");
let port = listener.local_addr().unwrap().port();
let address = format!("http://127.0.0.1:{}", port);
let mut configuration = get_configuration().expect("failed to read configuration");
configuration.database.database_name = Uuid::new_v4().to_string();
let connection_pool = PgPool::connect(&configuration.database.connection_string()).await.expect("failed to connect to postgres");
let server = run(listener, connection_pool.clone()).expect("failed to bind to address");
let _ = tokio::spawn(server);
TestApp{
address,
pool: connection_pool,
}
}
本书随后添加了以下更改:
//! tests/health_check.rs
let mut configuration = get_configuration().expect("Failed to read configuration.");
configuration.database.database_name = Uuid::new_v4().to_string();
let connection_pool = PgPool::connect(&configuration.database.connection_string())
.await
.expect("Failed to connect to Postgres.");
//! src/configuration.rs
// [...]
impl DatabaseSettings {
pub fn connection_string(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.username, self.password, self.host, self.port, self.database_name
)
}
pub fn connection_string_without_db(&self) -> String {
format!(
"postgres://{}:{}@{}:{}",
self.username, self.password, self.host, self.port
)
}
}
//! tests/health_check.rs
// [...]
use sqlx::{Connection, Executor, PgConnection, PgPool};
use zero2prod::configuration::{get_configuration, DatabaseSettings};
async fn spawn_app() -> TestApp {
// [...]
let mut configuration = get_configuration().expect("Failed to read configuration.");
configuration.database.database_name = Uuid::new_v4().to_string();
let connection_pool = configure_database(&configuration.database).await;
// [...]
}
pub async fn configure_database(config: &DatabaseSettings) -> PgPool {
// Create database
let mut connection = PgConnection::connect(&config.connection_string_without_db())
.await
.expect("Failed to connect to Postgres");
connection
.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str())
.await
.expect("Failed to create database.");
// Migrate database
let connection_pool = PgPool::connect(&config.connection_string())
.await
.expect("Failed to connect to Postgres.");
sqlx::migrate!("./migrations")
.run(&connection_pool)
.await
.expect("Failed to migrate the database");
connection_pool
}
我无法理解最后一个函数
configure_database
的作用以及添加该函数如何解决问题
如果他不创建新数据库并使用相同的数据库,他将无法进行全面的测试,因为他们的数据来自之前的测试。我也读过那本书,他正在使用 docker 容器进行 Postgress。他还可以删除 docker 容器并创建一个新数据库,而不是创建一个新的随机数据库,每次运行测试时,他都可以使用常量数据库名称。如果你问我的话,这是一个有点令人头痛的过程。
正如评论中其他人提到的,configure_database 执行以下操作:
为每个测试使用随机数据库名称 每个测试都配置为使用随机 UUID 作为数据库名称连接到唯一的数据库。这取代了以前的方法,其中所有测试都使用名为“newsletter”的单个数据库。更新的方法确保测试隔离并避免冲突。以下是涉及的详细步骤:
创建随机数据库名称: 在 DatabaseSettings 中为数据库名称生成随机 UUID。
连接到数据库服务器: 建立与 PostgreSQL 服务器的连接而不指定数据库名称。这是必要的,因为随机命名的数据库尚未创建。
创建数据库: 执行 SQL 命令以使用随机生成的 UUID 作为名称来创建新数据库。 执行数据库迁移
运行迁移脚本以设置数据库架构: 这包括创建订阅等表。
归还PgPool 返回配置为连接到新创建的数据库的连接池(PgPool)。然后测试使用它来生成应用程序。
每次测试使用随机数据库名称的好处 测试隔离:每个测试都针对自己的数据库运行,确保测试数据不会干扰其他测试。这消除了共享状态引起的不稳定。
避免唯一约束违规:在本地,您可以多次运行货物测试而不会遇到唯一约束违规。例如,如果之前的测试运行将包含电子邮件 [email protected] 的记录插入到
这种方法通过确保每个测试在干净且隔离的环境中运行来增强测试的可靠性和可重复性。