Repository: Brendonovich/prisma-client-rust (language: rust)

Issue:
Mock Client
Not that hard tbh
<img width="568" alt="image" src="https://user-images.githubusercontent.com/14191578/209846835-52680eca-39ca-4d23-93b2-ddc4d6a8beaa.png">



Discussion / hints:

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 5bfde921..7aeb1c9c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,7 +23,8 @@ resolver = "2"
 default = ["mysql", "sqlite", "mssql", "postgresql", "mongodb"]
 rspc = ["dep:rspc"]
 sqlite-create-many = ["psl/sqlite-create-many"]
-migrations = ["migration-core", "include_dir", "tempdir", "tokio", "tracing"]
+migrations = ["migration-core", "include_dir", "tempdir", "tokio/fs", "tracing"]
+mocking = ["tokio"]
 mutation-callbacks = []
 
 mysql = ["query-core/mysql", "migration-core/mysql"]
@@ -45,6 +46,7 @@ uuid = { version = "1", features = ["serde"] }
 indexmap = "1.8.2"
 serde-value = "0.7.0"
 base64 = "0.13.0"
+tokio = { version = "1.21.0", optional = true }
 
 query-core = { git = "https://github.com/Brendonovich/prisma-engines", rev = "43fcfd1b5c0be59e414fb03cf7b7712e1661b6d0" }
 query-connector = { git = "https://github.com/Brendonovich/prisma-engines", rev = "43fcfd1b5c0be59e414fb03cf7b7712e1661b6d0" }
@@ -60,7 +62,6 @@ diagnostics = { git = "https://github.com/Brendonovich/prisma-engines", rev = "4
 migration-core = { git = "https://github.com/Brendonovich/prisma-engines", rev = "43fcfd1b5c0be59e414fb03cf7b7712e1661b6d0", optional = true }
 include_dir = { version = "0.7.2", optional = true }
 tempdir = { version = "0.3.7", optional = true }
-tokio = { version = "1.21.0", features = ["fs"], optional = true }
 tracing = { version = "0.1.36", optional = true }
 
 # features = "rspc"
diff --git a/cli/Cargo.toml b/cli/Cargo.toml
index 23374728..b29438a9 100644
--- a/cli/Cargo.toml
+++ b/cli/Cargo.toml
@@ -11,6 +11,7 @@ rspc = []
 migrations = []
 sqlite-create-many = ["prisma-client-rust-sdk/sqlite-create-many"]
 mutation-callbacks = []
+mocking = []
 
 mysql = ["prisma-client-rust-sdk/mysql"]
 sqlite = ["prisma-client-rust-sdk/sqlite"]
diff --git a/cli/src/generator/client.rs b/cli/src/generator/client.rs
index e2cda309..68dce121 100644
--- a/cli/src/generator/client.rs
+++ b/cli/src/generator/client.rs
@@ -25,7 +25,7 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
     let migrate_fns = cfg!(feature = "migrations").then(|| {
         quote! {
             pub async fn _migrate_deploy(&self) -> Result<(), #pcr::migrations::MigrateDeployError> {
-                let res = #pcr::migrations::migrate_deploy(super::DATAMODEL_STR, super::MIGRATIONS_DIR, &self.0.url).await;
+                let res = #pcr::migrations::migrate_deploy(super::DATAMODEL_STR, super::MIGRATIONS_DIR, &self.0.url()).await;
 
                 // don't ask, just accept.
                 // migration engine seems to want some time to process things
@@ -35,11 +35,11 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
             }
 
             pub async fn _migrate_resolve(&self, migration: &str) -> Result<(), #pcr::migrations::MigrateResolveError> {
-                #pcr::migrations::migrate_resolve(migration, super::DATAMODEL_STR, super::MIGRATIONS_DIR, &self.0.url,).await
+                #pcr::migrations::migrate_resolve(migration, super::DATAMODEL_STR, super::MIGRATIONS_DIR, &self.0.url(),).await
             }
 
             pub fn _db_push(&self) -> #pcr::migrations::DbPush {
-                #pcr::migrations::db_push(super::DATAMODEL_STR, &self.0.url)
+                #pcr::migrations::db_push(super::DATAMODEL_STR, &self.0.url())
             }
         }
     });
@@ -53,6 +53,16 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
         }
     });
 
+    let mock_ctor = cfg!(feature = "mocking").then(|| {
+        quote! {
+            pub async fn _mock() -> (Self, #pcr::MockStore) {
+                let (internals, store) = #pcr::PrismaClientInternals::new_mock(#pcr::ActionNotifier::new()).await;
+
+                (Self(internals), store)
+            }
+        }
+    });
+
     quote! {
         pub struct PrismaClientBuilder {
             url: Option<String>,
@@ -60,7 +70,7 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
         }
 
         impl PrismaClientBuilder {
-            pub fn new() -> Self {
+            fn new() -> Self {
                 Self {
                     url: None,
                     action_notifier: #pcr::ActionNotifier::new()
@@ -99,6 +109,8 @@ pub fn generate(args: &GenerateArgs) -> TokenStream {
                 PrismaClientBuilder::new()
             }
 
+            #mock_ctor
+
             pub fn _query_raw<T: serde::de::DeserializeOwned>(&self, query: #pcr::Raw) -> #pcr::QueryRaw<T> {
                 #pcr::QueryRaw::new(
                     &self.0,
diff --git a/docs/src/config.ts b/docs/src/config.ts
index d22d7ee4..9608b46b 100644
--- a/docs/src/config.ts
+++ b/docs/src/config.ts
@@ -64,6 +64,7 @@ export const SIDEBAR = {
     ...section("Extra", [
       { text: "Raw Queries", link: 'raw' },
       { text: "Batching Queries", link: 'batching' },
+      { text: "Mocking Queries", link: 'mocking' },
       { text: "Error Handling", link: "error-handling" },
       { text: "Migrations", link: 'migrations' },
       { text: "rspc Integration", link: 'rspc' }
diff --git a/docs/src/pages/extra/mocking.md b/docs/src/pages/extra/mocking.md
new file mode 100644
index 00000000..0f571cec
--- /dev/null
+++ b/docs/src/pages/extra/mocking.md
@@ -0,0 +1,72 @@
+---
+title: Mocking Queries
+layout: ../../layouts/MainLayout.astro
+---
+
+When writing tests for function which use the Prisma client,
+it can be difficult to have a real database running to test with.
+To combat this, you can enable the `mocking` feature on `prisma-client-rust` and `prisma-client-rust-cli`
+and create a 'mock' client that runs queries using data you provide beforehand.
+This allows you to define the expected result of a query and then perform tests using those expected results.
+
+The examples use the following Prisma schema:
+
+```prisma
+model Post {
+    id    String   @default(cuid()) @id
+    title String
+}
+```
+
+Say you have a function `get_post_title`:
+
+```rust
+use prisma::{PrismaClient, post};
+
+async fn get_post_title(
+    client: &PrismaClient,
+    post_id: String,
+) -> prisma_client_rust::QueryResult<Option<String>> {
+    let post = client.post().find_unique(post::id::equals(post_id)).await?;
+
+    post.map(|post| post.title)
+}
+```
+
+To write a unit test,
+first create a mock client and mock store with `PrismaClient::_mock`,
+define your expectations via the mock store,
+and run the test.
+The mock client will use data from the mock store to resolve queries.
+
+```rust
+#[cfg(test)]
+mod test {
+    use super::*;
+
+    #[tokio::test]
+    async fn gets_title() -> prisma_client_rust::QueryResult<()> {
+        let (client, mock) = PrismaClient::_mock().await;
+
+        let id = "123".to_string();
+        let expected_title = "Test".to_string();
+
+        mock.expect(
+            // First argument is query without calling 'exec'
+            client.post().find_unique(post::id::equals(post_id)),
+            // Second argument is expected return type.
+            // This will fail to compile if it does not match
+            // the return type of the query
+            post::Data {
+                id: id.clone(),
+                title: expected_title.to_string(),
+            },
+        )
+        .await;
+
+       let title = get_post_title(&client, id).await?;
+
+       assert_eq!(title, Some(expected_title));
+    }
+}
+```
diff --git a/prisma-cli/Cargo.toml b/prisma-cli/Cargo.toml
index f441429f..c770470c 100644
--- a/prisma-cli/Cargo.toml
+++ b/prisma-cli/Cargo.toml
@@ -11,5 +11,6 @@ prisma-client-rust-cli = { features = [
   "sqlite-create-many",
   "sqlite",
   "migrations",
-  "mutation-callbacks"
+  "mutation-callbacks",
+  "mocking"
 ], default_features = false, path = "../cli" }
diff --git a/src/actions.rs b/src/actions.rs
index 2b0f88b3..006446ac 100644
--- a/src/actions.rs
+++ b/src/actions.rs
@@ -2,7 +2,66 @@ use prisma_models::PrismaValue;
 use query_core::{Selection, SelectionArgument};
 use serde::de::DeserializeOwned;
 
-use crate::{ModelActionType, ModelMutationType, SerializedWhereInput};
+use crate::SerializedWhereInput;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ModelQueryType {
+    FindUnique,
+    FindFirst,
+    FindMany,
+    Count,
+}
+
+impl ModelQueryType {
+    pub fn name(&self) -> &'static str {
+        match self {
+            Self::FindUnique => "findUnique",
+            Self::FindFirst => "findFirst",
+            Self::FindMany => "findMany",
+            Self::Count => "aggregate",
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ModelMutationType {
+    Create,
+    CreateMany,
+    Update,
+    UpdateMany,
+    Delete,
+    DeleteMany,
+    Upsert,
+}
+
+impl ModelMutationType {
+    pub fn name(&self) -> &'static str {
+        match self {
+            Self::Create => "createOne",
+            Self::CreateMany => "createMany",
+            Self::Update => "updateOne",
+            Self::UpdateMany => "updateMany",
+            Self::Delete => "deleteOne",
+            Self::DeleteMany => "deleteMany",
+            Self::Upsert => "upsertOne",
+        }
+    }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ModelActionType {
+    Query(ModelQueryType),
+    Mutation(ModelMutationType),
+}
+
+impl ModelActionType {
+    pub fn name(&self) -> &'static str {
+        match self {
+            Self::Query(q) => q.name(),
+            Self::Mutation(q) => q.name(),
+        }
+    }
+}
 
 pub trait ModelActions {
     type Data: DeserializeOwned;
diff --git a/src/client.rs b/src/client.rs
index 2b28850e..46f172ec 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -7,37 +7,93 @@ use serde::de::{DeserializeOwned, IntoDeserializer};
 use thiserror::Error;
 
 use crate::{
-    prisma_value, ActionNotifier, ModelAction, ModelActionType, ModelActions,
+    prisma_value, ActionNotifier, MockStore, ModelAction, ModelActionType, ModelActions,
     ModelMutationCallbackData, QueryError, Result,
 };
 
 pub type Executor = Box<dyn query_core::QueryExecutor + Send + Sync + 'static>;
 
+pub(crate) enum ExecutionEngine {
+    Real {
+        executor: Executor,
+        query_schema: Arc<QuerySchema>,
+        url: String,
+    },
+    Mock(MockStore),
+}
+
+impl ExecutionEngine {
+    async fn execute(&self, op: Operation) -> Result<serde_value::Value> {
+        match self {
+            Self::Real {
+                executor,
+                query_schema,
+                ..
+            } => {
+                let response = executor
+                    .execute(None, op, query_schema.clone(), None)
+                    .await
+                    .map_err(|e| QueryError::Execute(e.into()))?;
+
+                let data: prisma_value::Item = response.data.into();
+
+                Ok(serde_value::to_value(data)?)
+            }
+            Self::Mock(store) => Ok(store.get_op(&op).await.expect("Mock data not found")),
+        }
+    }
+
+    pub async fn execute_all(
+        &self,
+        ops: Vec<Operation>,
+    ) -> Result<Vec<Result<serde_value::Value>>> {
+        match self {
+            Self::Real {
+                executor,
+                query_schema,
+                ..
+            } => {
+                let response = executor
+                    .execute_all(None, ops, None, query_schema.clone(), None)
+                    .await
+                    .map_err(|e| QueryError::Execute(e.into()))?;
+
+                Ok(response
+                    .into_iter()
+                    .map(|result| {
+                        let data: prisma_value::Item = result
+                            .map_err(|e| QueryError::Execute(e.into()))?
+                            .data
+                            .into();
+
+                        Ok(serde_value::to_value(data)?)
+                    })
+                    .collect())
+            }
+            Self::Mock(store) => {
+                let mut ret = vec![];
+
+                for op in ops {
+                    ret.push(Ok(store.get_op(&op).await.expect("Mock data not found")))
+                }
+
+                Ok(ret)
+            }
+        }
+    }
+}
+
 /// The data held by the generated PrismaClient
 /// Do not use this in your own code!
 pub struct PrismaClientInternals {
-    pub executor: Executor,
-    pub query_schema: Arc<QuerySchema>,
-    pub url: String,
+    pub(crate) engine: ExecutionEngine,
     pub action_notifier: crate::ActionNotifier,
 }
 
 impl PrismaClientInternals {
-    // reduce monomorphization a lil bit
-    async fn execute_inner<'a>(&self, op: Operation) -> Result<serde_value::Value> {
-        let response = self
-            .executor
-            .execute(None, op, self.query_schema.clone(), None)
-            .await
-            .map_err(|e| QueryError::Execute(e.into()))?;
-
-        let data: prisma_value::Item = response.data.into();
-
-        Ok(serde_value::to_value(data)?)
-    }
-
     pub async fn execute<T: DeserializeOwned>(&self, operation: Operation) -> Result<T> {
-        let value = self.execute_inner(operation).await?;
+        // less monomorphization yay
+        let value = self.engine.execute(operation).await?;
 
         let ret = T::deserialize(value.into_deserializer())?;
 
@@ -109,12 +165,33 @@ impl PrismaClientInternals {
         executor.primary_connector().get_connection().await?;
 
         Ok(Self {
-            executor,
-            query_schema,
-            url,
+            engine: ExecutionEngine::Real {
+                executor,
+                query_schema,
+                url,
+            },
             action_notifier,
         })
     }
+
+    pub async fn new_mock(action_notifier: ActionNotifier) -> (Self, MockStore) {
+        let mock_store = MockStore::new();
+
+        (
+            Self {
+                engine: ExecutionEngine::Mock(mock_store.clone()),
+                action_notifier,
+            },
+            mock_store,
+        )
+    }
+
+    pub fn url(&self) -> &str {
+        match &self.engine {
+            ExecutionEngine::Mock(_) => "mock",
+            ExecutionEngine::Real { url, .. } => url,
+        }
+    }
 }
 
 trait DiagnosticsToString {
diff --git a/src/lib.rs b/src/lib.rs
index 54a3b591..706e7f6f 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -2,6 +2,7 @@ pub mod actions;
 mod client;
 #[cfg(feature = "migrations")]
 pub mod migrations;
+mod mock;
 pub mod operator;
 mod prisma_value;
 pub mod queries;
@@ -29,6 +30,7 @@ pub use user_facing_errors as prisma_errors;
 
 pub use actions::*;
 pub use client::*;
+pub use mock::*;
 pub use operator::Operator;
 pub use queries::*;
 pub use raw::*;
diff --git a/src/mock.rs b/src/mock.rs
new file mode 100644
index 00000000..18c37ba3
--- /dev/null
+++ b/src/mock.rs
@@ -0,0 +1,53 @@
+use std::sync::Arc;
+
+use query_core::{Operation, Selection};
+
+use serde::Serialize;
+use serde_value::Value;
+use tokio::sync::Mutex;
+
+use crate::BatchQuery;
+
+#[derive(Default, Clone)]
+pub struct MockStore {
+    read: Arc<Mutex<Vec<(Selection, Value)>>>,
+    write: Arc<Mutex<Vec<(Selection, Value)>>>,
+}
+
+impl MockStore {
+    pub fn new() -> Self {
+        Default::default()
+    }
+
+    // monomorphization optimisation moment
+    async fn add_op(&self, op: Operation, expected: Value) {
+        let (sel, mutex) = match op {
+            Operation::Read(sel) => (sel, &self.read),
+            Operation::Write(sel) => (sel, &self.write),
+        };
+
+        mutex.lock().await.push((sel, expected))
+    }
+
+    pub async fn expect<Q: BatchQuery>(&self, query: Q, expected: Q::RawType)
+    where
+        <Q as BatchQuery>::RawType: Serialize,
+    {
+        self.add_op(query.graphql(), serde_value::to_value(expected).unwrap())
+            .await;
+    }
+
+    pub(crate) async fn get_op(&self, op: &Operation) -> Option<Value> {
+        let (sel, mutex) = match op {
+            Operation::Read(sel) => (sel, &self.read),
+            Operation::Write(sel) => (sel, &self.write),
+        };
+
+        mutex
+            .lock()
+            .await
+            .iter()
+            .find(|(s, _)| s == sel)
+            .map(|(_, e)| e.clone())
+    }
+}
diff --git a/src/queries/batch.rs b/src/queries/batch.rs
index a7c348da..20443c15 100644
--- a/src/queries/batch.rs
+++ b/src/queries/batch.rs
@@ -6,39 +6,18 @@ use serde::{
     Deserialize,
 };
 
-use crate::{prisma_value, PrismaClientInternals, QueryError};
+use crate::PrismaClientInternals;
 
 pub async fn batch<T: BatchContainer<Marker>, Marker>(
     container: T,
     client: &PrismaClientInternals,
 ) -> super::Result<T::ReturnType> {
-    let response = client
-        .executor
-        .execute_all(
-            None,
-            container.graphql(),
-            None,
-            client.query_schema.clone(),
-            None,
-        )
-        .await;
-
-    let response = response.map_err(|e| QueryError::Execute(e.into()))?;
-
-    let data = response
+    let data = client
+        .engine
+        .execute_all(container.graphql())
+        .await?
         .into_iter()
-        .map(|result| {
-            let data: prisma_value::Item = result
-                .map_err(|e| QueryError::Execute(e.into()))?
-                .data
-                .into();
-
-            let val = serde_value::to_value(data)?;
-
-            Ok(<T::RawType as Deserialize>::deserialize(
-                val.into_deserializer(),
-            )?)
-        })
+        .map(|result| Ok(T::RawType::deserialize(result?.into_deserializer())?))
         .collect::<super::Result<VecDeque<_>>>()?;
 
     Ok(T::convert(data))
diff --git a/src/queries/mod.rs b/src/queries/mod.rs
index bd5e8e2e..99499470 100644
--- a/src/queries/mod.rs
+++ b/src/queries/mod.rs
@@ -34,65 +34,6 @@ pub use query_core::{schema::QuerySchemaRef, Operation, Selection};
 use thiserror::Error;
 use user_facing_errors::UserFacingError;
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ModelQueryType {
-    FindUnique,
-    FindFirst,
-    FindMany,
-    Count,
-}
-
-impl ModelQueryType {
-    pub fn name(&self) -> &'static str {
-        match self {
-            Self::FindUnique => "findUnique",
-            Self::FindFirst => "findFirst",
-            Self::FindMany => "findMany",
-            Self::Count => "aggregate",
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ModelMutationType {
-    Create,
-    CreateMany,
-    Update,
-    UpdateMany,
-    Delete,
-    DeleteMany,
-    Upsert,
-}
-
-impl ModelMutationType {
-    pub fn name(&self) -> &'static str {
-        match self {
-            Self::Create => "createOne",
-            Self::CreateMany => "createMany",
-            Self::Update => "updateOne",
-            Self::UpdateMany => "updateMany",
-            Self::Delete => "deleteOne",
-            Self::DeleteMany => "deleteMany",
-            Self::Upsert => "upsertOne",
-        }
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum ModelActionType {
-    Query(ModelQueryType),
-    Mutation(ModelMutationType),
-}
-
-impl ModelActionType {
-    pub fn name(&self) -> &'static str {
-        match self {
-            Self::Query(q) => q.name(),
-            Self::Mutation(q) => q.name(),
-        }
-    }
-}
-
 pub enum SerializedWhereValue {
     Object(Vec<(String, prisma_models::PrismaValue)>),
     List(Vec<prisma_models::PrismaValue>),


Test patch:
diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml
index 3cd8912e..002e424b 100644
--- a/integration-tests/Cargo.toml
+++ b/integration-tests/Cargo.toml
@@ -18,5 +18,6 @@ prisma-client-rust = { path = "..", features = [
   "sqlite-create-many",
   "sqlite",
   "migrations",
-  "mutation-callbacks"
+  "mutation-callbacks",
+  "mocking",
 ], default-features = false }
diff --git a/integration-tests/tests/lib.rs b/integration-tests/tests/lib.rs
index b0c637ca..9d07d5c1 100644
--- a/integration-tests/tests/lib.rs
+++ b/integration-tests/tests/lib.rs
@@ -29,6 +29,7 @@ mod find_first;
 mod find_many;
 mod find_unique;
 mod include;
+mod mock;
 mod r_hash;
 mod raw;
 mod select;
diff --git a/integration-tests/tests/mock.rs b/integration-tests/tests/mock.rs
new file mode 100644
index 00000000..a839c679
--- /dev/null
+++ b/integration-tests/tests/mock.rs
@@ -0,0 +1,60 @@
+use crate::db::*;
+use crate::utils::*;
+
+#[tokio::test]
+async fn returns() -> TestResult {
+    let (client, mock) = PrismaClient::_mock().await;
+
+    user::select!(basic_user { id name });
+
+    let expected = basic_user::Data {
+        id: "123".to_string(),
+        name: "Brendan".to_string(),
+    };
+
+    let query = || {
+        client
+            .user()
+            .find_unique(user::id::equals("123".to_string()))
+            .select(basic_user::select())
+    };
+
+    mock.expect(query(), Some(expected.clone())).await;
+
+    let result = query().exec().await?.unwrap();
+
+    assert_eq!(expected.id, result.id);
+    assert_eq!(expected.name, result.name);
+
+    Ok(())
+}
+
+#[tokio::test]
+async fn returns_many() -> TestResult {
+    let (client, mock) = PrismaClient::_mock().await;
+
+    user::select!(basic_user { id name });
+
+    let expected = basic_user::Data {
+        id: "123".to_string(),
+        name: "Brendan".to_string(),
+    };
+
+    let query = || {
+        client
+            .user()
+            .find_many(vec![user::name::equals("Brendan".to_string())])
+            .select(basic_user::select())
+    };
+
+    mock.expect(query(), vec![expected.clone()]).await;
+
+    let result = &query().exec().await?[0];
+
+    assert_eq!(expected.id, result.id);
+    assert_eq!(expected.name, result.name);
+
+    Ok(())
+}
+
+// TODO: Errors