Add unit test for pagination (#20)
* Add unit test for pagination * End with empty page * Add more test cases * Assert transaction log * Test utils * Feature gated * IntoMockRow, derive macro with into MockRow * Revert derive macro * WIP * WIP * assert_transaction_log
This commit is contained in:
parent
9058089dc8
commit
62fb43c605
@ -41,8 +41,14 @@ impl MockDatabase {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn append_query_results(mut self, mut vec: Vec<Vec<MockRow>>) -> Self {
|
||||
self.query_results.append(&mut vec);
|
||||
pub fn append_query_results<T>(mut self, vec: Vec<Vec<T>>) -> Self
|
||||
where
|
||||
T: IntoMockRow,
|
||||
{
|
||||
for row in vec.into_iter() {
|
||||
let row = row.into_iter().map(|vec| vec.into_mock_row()).collect();
|
||||
self.query_results.push(row);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
@ -80,6 +86,19 @@ impl MockDatabaseTrait for MockDatabase {
|
||||
Err(QueryErr)
|
||||
}
|
||||
}
|
||||
|
||||
fn into_transaction_log(&mut self) -> Vec<Statement> {
|
||||
std::mem::take(&mut self.transaction_log)
|
||||
}
|
||||
|
||||
fn assert_transaction_log(&mut self, stmts: Vec<Statement>) {
|
||||
for stmt in stmts.iter() {
|
||||
assert!(!self.transaction_log.is_empty());
|
||||
let log = self.transaction_log.first().unwrap();
|
||||
assert_eq!(log.to_string(), stmt.to_string());
|
||||
self.transaction_log = self.transaction_log.drain(1..).collect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockRow {
|
||||
@ -102,3 +121,16 @@ impl From<BTreeMap<&str, Value>> for MockRow {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait IntoMockRow {
|
||||
fn into_mock_row(self) -> MockRow;
|
||||
}
|
||||
|
||||
impl<T> IntoMockRow for T
|
||||
where
|
||||
T: Into<MockRow>,
|
||||
{
|
||||
fn into_mock_row(self) -> MockRow {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,18 @@ use std::sync::{
|
||||
pub struct MockDatabaseConnector;
|
||||
|
||||
pub struct MockDatabaseConnection {
|
||||
counter: AtomicUsize,
|
||||
mocker: Mutex<Box<dyn MockDatabaseTrait>>,
|
||||
pub(crate) counter: AtomicUsize,
|
||||
pub(crate) mocker: Mutex<Box<dyn MockDatabaseTrait>>,
|
||||
}
|
||||
|
||||
pub trait MockDatabaseTrait: Send {
|
||||
fn execute(&mut self, counter: usize, stmt: Statement) -> Result<ExecResult, ExecErr>;
|
||||
|
||||
fn query(&mut self, counter: usize, stmt: Statement) -> Result<Vec<QueryResult>, QueryErr>;
|
||||
|
||||
fn into_transaction_log(&mut self) -> Vec<Statement>;
|
||||
|
||||
fn assert_transaction_log(&mut self, stmts: Vec<Statement>);
|
||||
}
|
||||
|
||||
impl MockDatabaseConnector {
|
||||
|
@ -99,3 +99,239 @@ where
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "mock")]
|
||||
mod tests {
|
||||
use crate::entity::prelude::*;
|
||||
use crate::tests_cfg::{util::*, *};
|
||||
use crate::{Database, MockDatabase, QueryErr};
|
||||
use futures::TryStreamExt;
|
||||
use sea_query::{Alias, Expr, SelectStatement, Value};
|
||||
|
||||
fn setup() -> (Database, Vec<Vec<fruit::Model>>) {
|
||||
let page1 = vec![
|
||||
fruit::Model {
|
||||
id: 1,
|
||||
name: "Blueberry".into(),
|
||||
cake_id: Some(1),
|
||||
},
|
||||
fruit::Model {
|
||||
id: 2,
|
||||
name: "Rasberry".into(),
|
||||
cake_id: Some(1),
|
||||
},
|
||||
];
|
||||
|
||||
let page2 = vec![fruit::Model {
|
||||
id: 3,
|
||||
name: "Strawberry".into(),
|
||||
cake_id: Some(2),
|
||||
}];
|
||||
|
||||
let page3 = Vec::<fruit::Model>::new();
|
||||
|
||||
let db = MockDatabase::new()
|
||||
.append_query_results(vec![page1.clone(), page2.clone(), page3.clone()])
|
||||
.into_database();
|
||||
|
||||
(db, vec![page1, page2, page3])
|
||||
}
|
||||
|
||||
fn setup_num_rows() -> (Database, i32) {
|
||||
let num_rows = 3;
|
||||
let db = MockDatabase::new()
|
||||
.append_query_results(vec![vec![maplit::btreemap! {
|
||||
"num_rows" => Into::<Value>::into(num_rows),
|
||||
}]])
|
||||
.into_database();
|
||||
|
||||
(db, num_rows)
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn fetch_page() -> Result<(), QueryErr> {
|
||||
let (db, pages) = setup();
|
||||
|
||||
let paginator = fruit::Entity::find().paginate(&db, 2);
|
||||
|
||||
assert_eq!(paginator.fetch_page(0).await?, pages[0].clone());
|
||||
assert_eq!(paginator.fetch_page(1).await?, pages[1].clone());
|
||||
assert_eq!(paginator.fetch_page(2).await?, pages[2].clone());
|
||||
|
||||
let select = SelectStatement::new()
|
||||
.exprs(vec![
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Id),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Name),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::CakeId),
|
||||
])
|
||||
.from(fruit::Entity)
|
||||
.to_owned();
|
||||
|
||||
let query_builder = db.get_query_builder_backend();
|
||||
let stmts = vec![
|
||||
query_builder.build_select_statement(select.clone().offset(0).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(2).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(4).limit(2)),
|
||||
];
|
||||
let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap();
|
||||
mocker.assert_transaction_log(stmts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn fetch() -> Result<(), QueryErr> {
|
||||
let (db, pages) = setup();
|
||||
|
||||
let mut paginator = fruit::Entity::find().paginate(&db, 2);
|
||||
|
||||
assert_eq!(paginator.fetch().await?, pages[0].clone());
|
||||
paginator.next();
|
||||
|
||||
assert_eq!(paginator.fetch().await?, pages[1].clone());
|
||||
paginator.next();
|
||||
|
||||
assert_eq!(paginator.fetch().await?, pages[2].clone());
|
||||
|
||||
let select = SelectStatement::new()
|
||||
.exprs(vec![
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Id),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Name),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::CakeId),
|
||||
])
|
||||
.from(fruit::Entity)
|
||||
.to_owned();
|
||||
|
||||
let query_builder = db.get_query_builder_backend();
|
||||
let stmts = vec![
|
||||
query_builder.build_select_statement(select.clone().offset(0).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(2).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(4).limit(2)),
|
||||
];
|
||||
let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap();
|
||||
mocker.assert_transaction_log(stmts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn num_pages() -> Result<(), QueryErr> {
|
||||
let (db, num_rows) = setup_num_rows();
|
||||
|
||||
let num_rows = num_rows as usize;
|
||||
let page_size = 2_usize;
|
||||
let num_pages = (num_rows / page_size) + (num_rows % page_size > 0) as usize;
|
||||
let paginator = fruit::Entity::find().paginate(&db, page_size);
|
||||
|
||||
assert_eq!(paginator.num_pages().await?, num_pages);
|
||||
|
||||
let sub_query = SelectStatement::new()
|
||||
.exprs(vec![
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Id),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Name),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::CakeId),
|
||||
])
|
||||
.from(fruit::Entity)
|
||||
.to_owned();
|
||||
|
||||
let select = SelectStatement::new()
|
||||
.expr(Expr::cust("COUNT(*) AS num_rows"))
|
||||
.from_subquery(sub_query, Alias::new("sub_query"))
|
||||
.to_owned();
|
||||
|
||||
let query_builder = db.get_query_builder_backend();
|
||||
let stmts = vec![query_builder.build_select_statement(&select)];
|
||||
let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap();
|
||||
mocker.assert_transaction_log(stmts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn next_and_cur_page() -> Result<(), QueryErr> {
|
||||
let (db, _) = setup();
|
||||
|
||||
let mut paginator = fruit::Entity::find().paginate(&db, 2);
|
||||
|
||||
assert_eq!(paginator.cur_page(), 0);
|
||||
paginator.next();
|
||||
|
||||
assert_eq!(paginator.cur_page(), 1);
|
||||
paginator.next();
|
||||
|
||||
assert_eq!(paginator.cur_page(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn fetch_and_next() -> Result<(), QueryErr> {
|
||||
let (db, pages) = setup();
|
||||
|
||||
let mut paginator = fruit::Entity::find().paginate(&db, 2);
|
||||
|
||||
assert_eq!(paginator.cur_page(), 0);
|
||||
assert_eq!(paginator.fetch_and_next().await?, Some(pages[0].clone()));
|
||||
|
||||
assert_eq!(paginator.cur_page(), 1);
|
||||
assert_eq!(paginator.fetch_and_next().await?, Some(pages[1].clone()));
|
||||
|
||||
assert_eq!(paginator.cur_page(), 2);
|
||||
assert_eq!(paginator.fetch_and_next().await?, None);
|
||||
|
||||
let select = SelectStatement::new()
|
||||
.exprs(vec![
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Id),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Name),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::CakeId),
|
||||
])
|
||||
.from(fruit::Entity)
|
||||
.to_owned();
|
||||
|
||||
let query_builder = db.get_query_builder_backend();
|
||||
let stmts = vec![
|
||||
query_builder.build_select_statement(select.clone().offset(0).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(2).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(4).limit(2)),
|
||||
];
|
||||
let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap();
|
||||
mocker.assert_transaction_log(stmts);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn into_stream() -> Result<(), QueryErr> {
|
||||
let (db, pages) = setup();
|
||||
|
||||
let mut fruit_stream = fruit::Entity::find().paginate(&db, 2).into_stream();
|
||||
|
||||
assert_eq!(fruit_stream.try_next().await?, Some(pages[0].clone()));
|
||||
assert_eq!(fruit_stream.try_next().await?, Some(pages[1].clone()));
|
||||
assert_eq!(fruit_stream.try_next().await?, None);
|
||||
|
||||
drop(fruit_stream);
|
||||
|
||||
let select = SelectStatement::new()
|
||||
.exprs(vec![
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Id),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::Name),
|
||||
Expr::tbl(fruit::Entity, fruit::Column::CakeId),
|
||||
])
|
||||
.from(fruit::Entity)
|
||||
.to_owned();
|
||||
|
||||
let query_builder = db.get_query_builder_backend();
|
||||
let stmts = vec![
|
||||
query_builder.build_select_statement(select.clone().offset(0).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(2).limit(2)),
|
||||
query_builder.build_select_statement(select.clone().offset(4).limit(2)),
|
||||
];
|
||||
let mut mocker = get_mock_db_connection(&db).mocker.lock().unwrap();
|
||||
mocker.assert_transaction_log(stmts[0..1].to_vec());
|
||||
mocker.assert_transaction_log(stmts[1..].to_vec());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -72,8 +72,7 @@ mod tests {
|
||||
let db = MockDatabase::new()
|
||||
.append_query_results(vec![vec![maplit::btreemap! {
|
||||
"id" => Into::<Value>::into(128), "name" => Into::<Value>::into("apple")
|
||||
}
|
||||
.into()]])
|
||||
}]])
|
||||
.into_database();
|
||||
|
||||
assert_eq!(
|
||||
|
@ -4,3 +4,7 @@ pub mod cake;
|
||||
pub mod cake_filling;
|
||||
pub mod filling;
|
||||
pub mod fruit;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod util;
|
||||
|
52
src/tests_cfg/util.rs
Normal file
52
src/tests_cfg/util.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use crate::{
|
||||
tests_cfg::*, Database, DatabaseConnection, IntoMockRow, MockDatabaseConnection, MockRow,
|
||||
};
|
||||
use sea_query::Value;
|
||||
|
||||
impl From<cake_filling::Model> for MockRow {
|
||||
fn from(model: cake_filling::Model) -> Self {
|
||||
let map = maplit::btreemap! {
|
||||
"cake_id" => Into::<Value>::into(model.cake_id),
|
||||
"filling_id" => Into::<Value>::into(model.filling_id),
|
||||
};
|
||||
map.into_mock_row()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<cake::Model> for MockRow {
|
||||
fn from(model: cake::Model) -> Self {
|
||||
let map = maplit::btreemap! {
|
||||
"id" => Into::<Value>::into(model.id),
|
||||
"name" => Into::<Value>::into(model.name),
|
||||
};
|
||||
map.into_mock_row()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<filling::Model> for MockRow {
|
||||
fn from(model: filling::Model) -> Self {
|
||||
let map = maplit::btreemap! {
|
||||
"id" => Into::<Value>::into(model.id),
|
||||
"name" => Into::<Value>::into(model.name),
|
||||
};
|
||||
map.into_mock_row()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<fruit::Model> for MockRow {
|
||||
fn from(model: fruit::Model) -> Self {
|
||||
let map = maplit::btreemap! {
|
||||
"id" => Into::<Value>::into(model.id),
|
||||
"name" => Into::<Value>::into(model.name),
|
||||
"cake_id" => Into::<Value>::into(model.cake_id),
|
||||
};
|
||||
map.into_mock_row()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mock_db_connection(db: &Database) -> &MockDatabaseConnection {
|
||||
match db.get_connection() {
|
||||
DatabaseConnection::MockDatabaseConnection(mock_conn) => mock_conn,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user