Skip to main content

sz_orm_core/
mock.rs

1//! Mock 数据库连接 — 用于单元测试,无需真实数据库
2//!
3//! # 概述
4//!
5//! `MockConnection` 实现了 `Connection` trait,允许在测试中预设 SQL 查询的
6//! 预期结果,从而在不连接真实数据库的情况下测试业务逻辑。
7//!
8//! # 使用示例
9//!
10//! ```ignore
11//! use sz_orm_core::mock::{MockConnection, MockRow};
12//! use sz_orm_core::value::Value;
13//!
14//! let mut mock = MockConnection::new();
15//!
16//! // 预设查询结果
17//! mock.expect_query("SELECT * FROM users")
18//!     .with_rows(vec![
19//!         MockRow::from(vec![
20//!             ("id", Value::from(1i64)),
21//!             ("name", Value::from("Alice")),
22//!         ]),
23//!     ]);
24//!
25//! // 执行查询,返回预设结果
26//! let rows = mock.query("SELECT * FROM users").await.unwrap();
27//! assert_eq!(rows.len(), 1);
28//! ```
29
30use std::collections::VecDeque;
31use std::future::Future;
32use std::pin::Pin;
33
34use crate::pool::Connection;
35use crate::pool::QueryRows;
36use crate::value::Value;
37use crate::DbError;
38
39/// 一行 mock 数据
40///
41/// 由 `(列名, 值)` 对组成,与 `QueryRows` 的行格式一致。
42pub type MockRow = Vec<(&'static str, Value)>;
43
44/// 预设的查询期望
45#[derive(Clone)]
46struct QueryExpectation {
47    /// 匹配的 SQL(None 表示匹配任意 SQL,作为 fallback)
48    sql: Option<String>,
49    /// 返回的行数据
50    rows: Vec<MockRow>,
51    /// 影响的行数(用于 execute)
52    rows_affected: u64,
53}
54
55/// Mock 数据库连接
56///
57/// 用于单元测试,预设 SQL 查询的预期结果,无需真实数据库。
58///
59/// # 事务模拟
60///
61/// `MockConnection` 会跟踪 `begin`/`commit`/`rollback` 调用状态,
62/// 可通过 `was_committed()` / `was_rolled_back()` 断言事务行为。
63pub struct MockConnection {
64    /// 预设的查询期望队列(FIFO)
65    expectations: VecDeque<QueryExpectation>,
66    /// 已执行的 SQL 记录(用于断言)
67    executed_sql: Vec<String>,
68    /// 事务状态
69    in_transaction: bool,
70    committed: bool,
71    rolled_back: bool,
72    /// 当无匹配期望时的行为
73    fallback_behavior: FallbackBehavior,
74}
75
76/// 无匹配期望时的行为
77#[derive(Clone, Copy, Debug, Default)]
78pub enum FallbackBehavior {
79    /// 返回空结果(默认)
80    #[default]
81    Empty,
82    /// 返回错误
83    Error,
84}
85
86impl MockConnection {
87    /// 创建新的空 MockConnection
88    pub fn new() -> Self {
89        Self {
90            expectations: VecDeque::new(),
91            executed_sql: Vec::new(),
92            in_transaction: false,
93            committed: false,
94            rolled_back: false,
95            fallback_behavior: FallbackBehavior::Empty,
96        }
97    }
98
99    /// 设置无匹配期望时的行为
100    pub fn with_fallback(mut self, behavior: FallbackBehavior) -> Self {
101        self.fallback_behavior = behavior;
102        self
103    }
104
105    /// 预设一个查询期望
106    ///
107    /// 返回 `QueryExpectationBuilder` 用于链式配置结果。
108    ///
109    /// # 示例
110    ///
111    /// ```ignore
112    /// mock.expect_query("SELECT * FROM users WHERE id = ?")
113    ///     .with_rows(vec![vec![("id", Value::from(1i64))]]);
114    /// ```
115    pub fn expect_query(&mut self, sql: impl Into<String>) -> QueryExpectationBuilder<'_> {
116        QueryExpectationBuilder {
117            mock: self,
118            sql: Some(sql.into()),
119            rows: vec![],
120            rows_affected: 0,
121        }
122    }
123
124    /// 预设一个匹配任意 SQL 的 fallback 期望
125    pub fn expect_any(&mut self) -> QueryExpectationBuilder<'_> {
126        QueryExpectationBuilder {
127            mock: self,
128            sql: None,
129            rows: vec![],
130            rows_affected: 0,
131        }
132    }
133
134    /// 预设 execute 的影响行数
135    pub fn expect_execute(&mut self, sql: impl Into<String>, rows_affected: u64) {
136        self.expectations.push_back(QueryExpectation {
137            sql: Some(sql.into()),
138            rows: vec![],
139            rows_affected,
140        });
141    }
142
143    /// 是否处于事务中
144    pub fn in_transaction(&self) -> bool {
145        self.in_transaction
146    }
147
148    /// 是否已提交
149    pub fn was_committed(&self) -> bool {
150        self.committed
151    }
152
153    /// 是否已回滚
154    pub fn was_rolled_back(&self) -> bool {
155        self.rolled_back
156    }
157
158    /// 获取已执行的 SQL 列表(用于断言)
159    pub fn executed_sql(&self) -> &[String] {
160        &self.executed_sql
161    }
162
163    /// 断言某 SQL 被执行了恰好 n 次
164    pub fn assert_executed_count(&self, sql: &str, count: usize) {
165        let actual = self.executed_sql.iter().filter(|s| *s == sql).count();
166        assert!(
167            actual == count,
168            "SQL `{}` 预期执行 {} 次,实际 {} 次",
169            sql,
170            count,
171            actual
172        );
173    }
174
175    /// 查找匹配的期望(精确匹配优先,fallback 兜底)
176    fn find_expectation(&mut self, sql: &str) -> Option<QueryExpectation> {
177        // 精确匹配
178        if let Some(pos) = self
179            .expectations
180            .iter()
181            .position(|e| e.sql.as_deref() == Some(sql))
182        {
183            return Some(self.expectations.remove(pos).unwrap()); // SAFETY: pos 来自 position() 保证有效,remove 一定返回 Some
184        }
185        // Fallback 匹配
186        if let Some(pos) = self.expectations.iter().position(|e| e.sql.is_none()) {
187            return Some(self.expectations.remove(pos).unwrap()); // SAFETY: pos 来自 position() 保证有效,remove 一定返回 Some
188        }
189        None
190    }
191
192    fn handle_query(&mut self, sql: &str) -> Result<QueryRows, DbError> {
193        self.executed_sql.push(sql.to_string());
194        match self.find_expectation(sql) {
195            Some(exp) => Ok(exp
196                .rows
197                .into_iter()
198                .map(|row| row.into_iter().map(|(k, v)| (k.to_string(), v)).collect())
199                .collect()),
200            None => match self.fallback_behavior {
201                FallbackBehavior::Empty => Ok(Vec::new()),
202                FallbackBehavior::Error => Err(DbError::Internal(format!(
203                    "MockConnection: 未预设的查询 `{}`",
204                    sql
205                ))),
206            },
207        }
208    }
209
210    fn handle_execute(&mut self, sql: &str) -> Result<u64, DbError> {
211        self.executed_sql.push(sql.to_string());
212        match self.find_expectation(sql) {
213            Some(exp) => Ok(exp.rows_affected),
214            None => match self.fallback_behavior {
215                FallbackBehavior::Empty => Ok(0),
216                FallbackBehavior::Error => Err(DbError::Internal(format!(
217                    "MockConnection: 未预设的 execute `{}`",
218                    sql
219                ))),
220            },
221        }
222    }
223}
224
225impl Default for MockConnection {
226    fn default() -> Self {
227        Self::new()
228    }
229}
230
231/// `expect_query` 的构建器
232pub struct QueryExpectationBuilder<'a> {
233    mock: &'a mut MockConnection,
234    sql: Option<String>,
235    rows: Vec<MockRow>,
236    rows_affected: u64,
237}
238
239impl<'a> QueryExpectationBuilder<'a> {
240    /// 设置返回的行数据
241    pub fn with_rows(mut self, rows: Vec<MockRow>) -> &'a mut MockConnection {
242        self.mock.expectations.push_back(QueryExpectation {
243            sql: self.sql.take(),
244            rows,
245            rows_affected: self.rows_affected,
246        });
247        self.mock
248    }
249
250    /// 设置影响行数(用于 INSERT/UPDATE/DELETE)
251    pub fn with_rows_affected(mut self, n: u64) -> &'a mut MockConnection {
252        self.rows_affected = n;
253        let rows = std::mem::take(&mut self.rows);
254        self.with_rows(rows)
255    }
256}
257
258// ---------------------------------------------------------------------------
259// Connection trait 实现
260// ---------------------------------------------------------------------------
261
262impl Connection for MockConnection {
263    fn execute<'a>(
264        &'a mut self,
265        sql: &'a str,
266    ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
267        Box::pin(async move { self.handle_execute(sql) })
268    }
269
270    fn query<'a>(
271        &'a mut self,
272        sql: &'a str,
273    ) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
274        Box::pin(async move { self.handle_query(sql) })
275    }
276
277    fn query_with_params<'a>(
278        &'a mut self,
279        sql: &'a str,
280        _params: &'a [crate::value::Value],
281    ) -> Pin<Box<dyn Future<Output = Result<QueryRows, DbError>> + Send + 'a>> {
282        // MockConnection 将 query_with_params 委托给 query(mock 不实际绑定参数)
283        Box::pin(async move { self.handle_query(sql) })
284    }
285
286    fn execute_with_params<'a>(
287        &'a mut self,
288        sql: &'a str,
289        _params: &'a [crate::value::Value],
290    ) -> Pin<Box<dyn Future<Output = Result<u64, DbError>> + Send + 'a>> {
291        // MockConnection 将 execute_with_params 委托给 execute(mock 不实际绑定参数)
292        Box::pin(async move { self.handle_execute(sql) })
293    }
294
295    fn begin_transaction<'a>(
296        &'a mut self,
297    ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
298        Box::pin(async move {
299            if self.in_transaction {
300                return Err(DbError::Internal("MockConnection: 已在事务中".to_string()));
301            }
302            self.in_transaction = true;
303            self.committed = false;
304            self.rolled_back = false;
305            Ok(())
306        })
307    }
308
309    fn commit<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
310        Box::pin(async move {
311            if !self.in_transaction {
312                return Err(DbError::Internal("MockConnection: 未开启事务".to_string()));
313            }
314            self.in_transaction = false;
315            self.committed = true;
316            Ok(())
317        })
318    }
319
320    fn rollback<'a>(
321        &'a mut self,
322    ) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
323        Box::pin(async move {
324            if !self.in_transaction {
325                return Err(DbError::Internal("MockConnection: 未开启事务".to_string()));
326            }
327            self.in_transaction = false;
328            self.rolled_back = true;
329            Ok(())
330        })
331    }
332
333    fn is_connected(&self) -> bool {
334        true
335    }
336
337    fn ping<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = bool> + Send + 'a>> {
338        Box::pin(async move { true })
339    }
340
341    fn close<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<(), DbError>> + Send + 'a>> {
342        Box::pin(async move { Ok(()) })
343    }
344}
345
346// ---------------------------------------------------------------------------
347// 单元测试
348// ---------------------------------------------------------------------------
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::value::Value;
354
355    #[tokio::test]
356    async fn test_mock_connection_basic_query() {
357        let mut mock = MockConnection::new();
358        mock.expect_query("SELECT * FROM users")
359            .with_rows(vec![vec![
360                ("id", Value::from(1i64)),
361                ("name", Value::from("Alice")),
362            ]]);
363
364        let rows = mock.query("SELECT * FROM users").await.unwrap();
365        assert_eq!(rows.len(), 1);
366        let row = rows.first().unwrap();
367        assert_eq!(row.get("id"), Some(&Value::from(1i64)));
368        assert_eq!(row.get("name"), Some(&Value::from("Alice")));
369    }
370
371    #[tokio::test]
372    async fn test_mock_connection_execute() {
373        let mut mock = MockConnection::new();
374        mock.expect_execute("INSERT INTO users (name) VALUES ('Bob')", 1);
375
376        let affected = mock
377            .execute("INSERT INTO users (name) VALUES ('Bob')")
378            .await
379            .unwrap();
380        assert_eq!(affected, 1);
381    }
382
383    #[tokio::test]
384    async fn test_mock_connection_fallback_empty() {
385        let mut mock = MockConnection::new();
386        // 未预设任何期望,默认返回空结果
387        let rows = mock.query("SELECT * FROM anything").await.unwrap();
388        assert_eq!(rows.len(), 0);
389    }
390
391    #[tokio::test]
392    async fn test_mock_connection_fallback_error() {
393        let mut mock = MockConnection::new().with_fallback(FallbackBehavior::Error);
394        let result = mock.query("SELECT * FROM anything").await;
395        assert!(result.is_err());
396    }
397
398    #[tokio::test]
399    async fn test_mock_connection_transaction_commit() {
400        let mut mock = MockConnection::new();
401        assert!(!mock.in_transaction());
402
403        mock.begin_transaction().await.unwrap();
404        assert!(mock.in_transaction());
405        assert!(!mock.was_committed());
406
407        mock.commit().await.unwrap();
408        assert!(!mock.in_transaction());
409        assert!(mock.was_committed());
410        assert!(!mock.was_rolled_back());
411    }
412
413    #[tokio::test]
414    async fn test_mock_connection_transaction_rollback() {
415        let mut mock = MockConnection::new();
416        mock.begin_transaction().await.unwrap();
417        mock.rollback().await.unwrap();
418        assert!(!mock.in_transaction());
419        assert!(!mock.was_committed());
420        assert!(mock.was_rolled_back());
421    }
422
423    #[tokio::test]
424    async fn test_mock_connection_double_begin_errors() {
425        let mut mock = MockConnection::new();
426        mock.begin_transaction().await.unwrap();
427        let result = mock.begin_transaction().await;
428        assert!(result.is_err());
429    }
430
431    #[tokio::test]
432    async fn test_mock_connection_commit_without_transaction_errors() {
433        let mut mock = MockConnection::new();
434        let result = mock.commit().await;
435        assert!(result.is_err());
436    }
437
438    #[tokio::test]
439    async fn test_mock_connection_executed_sql_tracking() {
440        let mut mock = MockConnection::new();
441        mock.expect_query("SELECT 1").with_rows(vec![]);
442        mock.query("SELECT 1").await.unwrap();
443        mock.query("SELECT 2").await.unwrap();
444
445        assert_eq!(mock.executed_sql().len(), 2);
446        assert_eq!(mock.executed_sql()[0], "SELECT 1");
447        assert_eq!(mock.executed_sql()[1], "SELECT 2");
448    }
449
450    #[tokio::test]
451    async fn test_mock_connection_assert_executed_count() {
452        let mut mock = MockConnection::new();
453        mock.expect_query("SELECT x").with_rows(vec![]);
454        mock.query("SELECT x").await.unwrap();
455        mock.query("SELECT x").await.unwrap();
456        mock.query("SELECT y").await.unwrap();
457
458        mock.assert_executed_count("SELECT x", 2);
459        mock.assert_executed_count("SELECT y", 1);
460    }
461
462    #[tokio::test]
463    async fn test_mock_connection_expect_any_fallback() {
464        let mut mock = MockConnection::new();
465        // 预设一个匹配任意 SQL 的 fallback
466        mock.expectations.push_back(QueryExpectation {
467            sql: None,
468            rows: vec![vec![("a", Value::from(42i64))]],
469            rows_affected: 0,
470        });
471
472        let rows = mock.query("ANY RANDOM SQL").await.unwrap();
473        assert_eq!(rows.len(), 1);
474    }
475
476    #[tokio::test]
477    async fn test_mock_connection_is_connected() {
478        let mock = MockConnection::new();
479        assert!(mock.is_connected());
480    }
481
482    #[tokio::test]
483    async fn test_mock_connection_ping() {
484        let mut mock = MockConnection::new();
485        assert!(mock.ping().await);
486    }
487
488    #[tokio::test]
489    async fn test_mock_connection_close() {
490        let mut mock = MockConnection::new();
491        assert!(mock.close().await.is_ok());
492    }
493
494    #[tokio::test]
495    async fn test_mock_connection_multiple_rows() {
496        let mut mock = MockConnection::new();
497        mock.expect_query("SELECT * FROM products").with_rows(vec![
498            vec![("id", Value::from(1i64)), ("name", Value::from("Widget"))],
499            vec![("id", Value::from(2i64)), ("name", Value::from("Gadget"))],
500            vec![
501                ("id", Value::from(3i64)),
502                ("name", Value::from("Doohickey")),
503            ],
504        ]);
505
506        let rows = mock.query("SELECT * FROM products").await.unwrap();
507        assert_eq!(rows.len(), 3);
508        assert_eq!(
509            rows.first().unwrap().get("name"),
510            Some(&Value::from("Widget"))
511        );
512        assert_eq!(
513            rows.get(2).unwrap().get("name"),
514            Some(&Value::from("Doohickey"))
515        );
516    }
517
518    #[tokio::test]
519    async fn test_mock_connection_exact_match_before_fallback() {
520        let mut mock = MockConnection::new();
521        // 先加 fallback
522        mock.expectations.push_back(QueryExpectation {
523            sql: None,
524            rows: vec![vec![("source", Value::from("fallback"))]],
525            rows_affected: 0,
526        });
527        // 再加精确匹配
528        mock.expect_query("SELECT exact")
529            .with_rows(vec![vec![("source", Value::from("exact"))]]);
530
531        let rows = mock.query("SELECT exact").await.unwrap();
532        assert_eq!(
533            rows.first().unwrap().get("source"),
534            Some(&Value::from("exact"))
535        );
536    }
537}