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:
Billy Chan 2021-06-18 23:43:07 +08:00 committed by GitHub
parent 9058089dc8
commit 62fb43c605
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 333 additions and 6 deletions

View File

@ -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()
}
}

View File

@ -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 {

View File

@ -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(())
}
}

View File

@ -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!(

View File

@ -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
View 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!(),
}
}