├── .env.example ├── .envrc.example ├── .github ├── pull_request_template.md └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── docker-compose.yml ├── examples └── mysql │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── fixtures │ └── todos.yml │ └── src │ └── main.rs ├── initdb.d └── initialize.sql ├── src ├── fixture_file.rs ├── helper.rs ├── lib.rs ├── loader.rs ├── mysql │ ├── helper.rs │ ├── loader.rs │ └── mod.rs └── postgresql │ ├── helper.rs │ ├── loader.rs │ └── mod.rs └── tests └── mysql.rs /.env.example: -------------------------------------------------------------------------------- 1 | TEST_DB_URL=mysql://root@127.0.0.1:3314/test 2 | TEST_DB_URL_FOR_DB_CHECK=mysql://root@127.0.0.1:3314/fizz 3 | -------------------------------------------------------------------------------- /.envrc.example: -------------------------------------------------------------------------------- 1 | dotenv 2 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Why 2 | 3 | # What 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - v* 9 | 10 | jobs: 11 | format: 12 | name: Format 13 | runs-on: ubuntu-20.04 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Cargo fmt 17 | uses: actions-rs/cargo@v1 18 | with: 19 | command: fmt 20 | args: --all -- --check 21 | 22 | check: 23 | name: Check 24 | runs-on: ubuntu-20.04 25 | strategy: 26 | matrix: 27 | runtime: [async-std, tokio] 28 | steps: 29 | - uses: actions/checkout@v2 30 | - name: Cargo check 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: check 34 | args: | 35 | --no-default-features 36 | --features runtime-${{ matrix.runtime }} 37 | 38 | lint: 39 | name: Lint 40 | runs-on: ubuntu-20.04 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Install dependencies 44 | run: | 45 | sudo apt-get install libssl-dev 46 | - name: Cache cargo registry 47 | uses: actions/cache@v1 48 | with: 49 | path: ~/.cargo/registry 50 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 51 | - name: Cache cargo index 52 | uses: actions/cache@v1 53 | with: 54 | path: ~/.cargo/git 55 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 56 | - name: Cache cargo build 57 | uses: actions/cache@v1 58 | with: 59 | path: target 60 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 61 | - name: Add clippy 62 | run: rustup component add clippy 63 | - name: Run lint 64 | uses: actions-rs/cargo@v1 65 | with: 66 | command: clippy 67 | 68 | test: 69 | name: Unit Test 70 | runs-on: ubuntu-20.04 71 | strategy: 72 | matrix: 73 | runtime: [async-std, tokio] 74 | needs: check 75 | steps: 76 | - uses: actions/checkout@v2 77 | - name: Cache cargo registry 78 | uses: actions/cache@v1 79 | with: 80 | path: ~/.cargo/registry 81 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 82 | - name: Cache cargo index 83 | uses: actions/cache@v1 84 | with: 85 | path: ~/.cargo/git 86 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 87 | - name: Cache cargo build 88 | uses: actions/cache@v1 89 | with: 90 | path: target 91 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 92 | - name: Build 93 | uses: actions-rs/cargo@v1 94 | with: 95 | command: build 96 | - name: Test 97 | uses: actions-rs/cargo@v1 98 | with: 99 | command: test 100 | args: | 101 | --no-default-features 102 | --features runtime-${{ matrix.runtime }} 103 | --no-fail-fast 104 | --color always 105 | 106 | mysql: 107 | name: MySQL 108 | runs-on: ubuntu-20.04 109 | strategy: 110 | matrix: 111 | mysql: [8, 5.7, 5.6] 112 | runtime: [async-std, tokio] 113 | needs: check 114 | services: 115 | mysql: 116 | image: mysql:${{ matrix.mysql }} 117 | env: 118 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 119 | MYSQL_DATABASE: "test" 120 | ports: 121 | - "3314:3306" 122 | options: -v ${{ github.workspace }}/initdb.d:/docker-entrypoint-initdb.d 123 | steps: 124 | - uses: actions/checkout@v2 125 | - name: Cache cargo registry 126 | uses: actions/cache@v1 127 | with: 128 | path: ~/.cargo/registry 129 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 130 | - name: Cache cargo index 131 | uses: actions/cache@v1 132 | with: 133 | path: ~/.cargo/git 134 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 135 | - name: Cache cargo build 136 | uses: actions/cache@v1 137 | with: 138 | path: target 139 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 140 | - name: Build 141 | uses: actions-rs/cargo@v1 142 | with: 143 | command: build 144 | args: | 145 | --features mysql 146 | - name: Initialize DB 147 | run: mysql --host $TEST_DB_HOST --port $TEST_DB_PORT -uroot test < $INIT_TEST_DB_PATH 148 | env: 149 | INIT_TEST_DB_PATH: ${{ github.workspace }}/initdb.d/initialize.sql 150 | TEST_DB_HOST: 127.0.0.1 151 | TEST_DB_PORT: 3314 152 | - name: Test 153 | uses: actions-rs/cargo@v1 154 | env: 155 | TEST_DB_URL: mysql://root@127.0.0.1:3314/test 156 | TEST_DB_URL_FOR_DB_CHECK: mysql://root@127.0.0.1:3314/fizz 157 | with: 158 | command: test 159 | args: | 160 | --no-default-features 161 | --features mysql,runtime-${{ matrix.runtime }} 162 | --no-fail-fast 163 | --color always 164 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | jobs: 7 | check: 8 | name: Check 9 | runs-on: ubuntu-20.04 10 | strategy: 11 | matrix: 12 | runtime: [async-std, tokio] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Cargo check 16 | uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | args: | 20 | --no-default-features 21 | --features runtime-${{ matrix.runtime }} 22 | 23 | release: 24 | name: Cargo publish 25 | runs-on: ubuntu-20.04 26 | needs: check 27 | steps: 28 | - uses: actions/checkout@v1 29 | - run: cargo login ${CRATES_IO_TOKEN} 30 | env: 31 | CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} 32 | - run: cargo publish 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .envrc 3 | .env 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | ".", 4 | "examples/mysql/", 5 | ] 6 | 7 | [package] 8 | name = "testfixtures" 9 | version = "0.1.2" 10 | authors = ["Takayuki Maeda "] 11 | edition = "2018" 12 | license = "MIT" 13 | description = "A library for preparing test data from yaml files in Rust" 14 | repository = "https://github.com/TaKO8Ki/testfixtures" 15 | documentation = "https://docs.rs/testfixtures" 16 | readme = "README.md" 17 | keywords = [ "test", "testdata", "db", "sqlx", "mysql" ] 18 | categories = [ "database" ] 19 | 20 | [dependencies] 21 | sqlx = { version = "0.3", default-features = false, features = [ "mysql", "postgres", "chrono", "macros" ] } 22 | yaml-rust = "0.4" 23 | anyhow = "1.0" 24 | futures = "0.1" 25 | async-trait = "0.1.31" 26 | regex = "1" 27 | chrono = "0.4.11" 28 | 29 | [dev-dependencies] 30 | async-std = { version = "1.5.0", features = [ "attributes" ] } 31 | tokio = { version = "0.2.21", features = [ "full" ] } 32 | tempfile = "3" 33 | 34 | [features] 35 | default = [ "runtime-async-std" ] 36 | runtime-tokio = [ "sqlx/runtime-tokio" ] 37 | runtime-async-std = [ "sqlx/runtime-async-std" ] 38 | mysql = [] 39 | 40 | [[test]] 41 | name = "mysql" 42 | path = "tests/mysql.rs" 43 | required-features = [ "mysql" ] 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Takayuki Maeda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | cargo test --no-fail-fast -- --nocapture 3 | 4 | # you need to set environment variables. 5 | mysql/test: 6 | cargo test --features mysql --no-fail-fast -- --nocapture 7 | 8 | db: 9 | docker-compose up -d 10 | 11 | env: 12 | cp .env.example .env 13 | cp .envrc.example .envrc 14 | 15 | doc: 16 | cargo doc --no-deps --open 17 | 18 | mysql: 19 | mysql --host 127.0.0.1 --port 3314 -uroot test 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

testfixtures

2 |
3 | 4 | testfixtures is a Rust library for preparing test data from yaml files. 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 |
26 |

27 | 28 | Usage 29 | 30 | | 31 | 32 | Docs 33 | 34 | | 35 | 36 | Examples 37 | 38 |

39 |
40 | 41 | ## Install 42 | 43 | This crate is compatible with the async-std and tokio runtimes. 44 | 45 | async-std 46 | 47 | ```toml 48 | [dependencies] 49 | testfixtures = "0.1" 50 | sqlx = "0.3" 51 | chrono = "0.4.11" 52 | ``` 53 | 54 | tokio 55 | 56 | ```toml 57 | [dependencies] 58 | testfixtures = { version = "0.1", default-features = false, features = [ "runtime-tokio" ] } 59 | sqlx = { version = "0.3", default-features = false, features = [ "runtime-tokio", "macros" ] } 60 | chrono = "0.4.11" 61 | ``` 62 | 63 | ## Usage 64 | 65 | Create fixture files like the following. 66 | Fixture files should have the name `.yml`. 67 | 68 | `todos.yml` 69 | ```yml 70 | - id: 1 71 | description: buy a new camera 72 | done: true 73 | progress: 10.5 74 | created_at: 2020/01/01 01:01:01 75 | - id: 2 76 | description: meeting 77 | done: false 78 | progress: 30.0 79 | created_at: 2020/01/01 02:02:02 80 | ``` 81 | 82 |
Click and see the datetime format example
83 | 84 | ```rust 85 | 2020-01-01 01:01 86 | 2020-01-01 01:01:01 87 | 20200101 01:01 88 | 20200101 01:01:01 89 | 01012020 01:01 90 | 01012020 01:01:01 91 | 2020/01/01 01:01 92 | 2020/01/01 01:01:01 93 | ``` 94 |
95 | 96 | If you need to write raw SQL, probably to call a function, prefix the value of the column with RAW=. 97 | 98 | ```yml 99 | - id: 1 100 | description: fizz 101 | done: true 102 | progress: 10.5 103 | created_at: RAW=NOW() 104 | ``` 105 | 106 | Your tests would look like this. 107 | 108 | ```rust 109 | 110 | #[cfg(test)] 111 | mod tests { 112 | use chrono::Utc; 113 | use sqlx::MySqlPool; 114 | use std::env; 115 | use testfixtures::MySqlLoader; 116 | 117 | #[async_std::test] 118 | async fn test_something() -> anyhow::Result<()> { 119 | let pool = MySqlPool::new(&env::var("DATABASE_URL")?).await?; 120 | let loader = MySqlLoader::new(|cfg| { 121 | cfg.location(Utc); 122 | cfg.database(pool); 123 | cfg.skip_test_database_check(); 124 | cfg.paths(vec!["fixtures/todos.yml"]); 125 | }) 126 | .await?; 127 | 128 | // load your fixtures 129 | loader.load().await.unwrap(); 130 | 131 | // run your tests 132 | 133 | Ok(()) 134 | } 135 | } 136 | 137 | ``` 138 | 139 | **PgLoader** and **SqliteLoader** are under development. 140 | 141 | ## Options 142 | 143 | ### database(required) 144 | database is a option for passing db connection pool to a Loader. 145 | 146 | ```rust 147 | let pool = MySqlPool::new(&env::var("DATABASE_URL")?).await?; 148 | let loader = MySqlLoader::new(|cfg| { 149 | cfg.database(pool); 150 | // ... 151 | }) 152 | .await?; 153 | ``` 154 | 155 | ### location(required) 156 | location is a option for setting timezone. 157 | 158 | ```rust 159 | use chrono::Utc; 160 | 161 | let loader = MySqlLoader::new(|cfg| { 162 | cfg.location(Utc); 163 | // or cfg.location(Local); 164 | // ... 165 | }) 166 | .await?; 167 | ``` 168 | 169 | ### skip_test_database_check(optional) 170 | skip_test_database_check is a option for setting a flag for checking if database name ends with "test". 171 | 172 | ```rust 173 | let loader = MySqlLoader::new(|cfg| { 174 | cfg.skip_test_database_check(); 175 | // ... 176 | }) 177 | .await?; 178 | ``` 179 | 180 | ### files(optional) 181 | files is a option for reading your fixture files. 182 | 183 | ```rust 184 | let loader = MySqlLoader::new(|cfg| { 185 | cfg.files(vec!["fizz.yml"]); 186 | // ... 187 | }) 188 | .await?; 189 | ``` 190 | 191 | ### directory(optional) 192 | files is a option for reading your fixture files in a directory. 193 | 194 | ```rust 195 | let loader = MySqlLoader::new(|cfg| { 196 | cfg.directory("fixture"); 197 | // ... 198 | }) 199 | .await?; 200 | ``` 201 | 202 | ### paths(optional) 203 | paths is a option that is a combination of files option and directory option. 204 | 205 | ```rust 206 | let loader = MySqlLoader::new(|cfg| { 207 | cfg.paths(vec!["fizz", "buzz/todos.yml"]); 208 | // ... 209 | }) 210 | .await?; 211 | ``` 212 | 213 | ## Implemation status 214 | ### Database 215 | - [x] MySQL and MariaDB 216 | - [ ] Postgres 217 | - [ ] SQLite 218 | 219 | ### Options 220 | - [x] database 221 | - [x] load files 222 | - [x] skip_test_database_check 223 | - [x] location 224 | - [x] directory 225 | - [x] paths 226 | - [ ] template 227 | 228 | ## Contribution 229 | 230 | ```sh 231 | # setup test db 232 | $ make db 233 | 234 | # load environment variables 235 | $ make env 236 | $ direnv allow # https://github.com/direnv/direnv 237 | 238 | # run unit tests 239 | $ make test 240 | ``` 241 | 242 | ### Show your support 243 | Give a ⭐️ if this project helped you! 244 | 245 | ## Reference 246 | https://github.com/go-testfixtures/testfixtures 247 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | db: 4 | image: mysql:5.7 5 | volumes: 6 | - ./initdb.d:/docker-entrypoint-initdb.d 7 | environment: 8 | MYSQL_ALLOW_EMPTY_PASSWORD: "yes" 9 | MYSQL_DATABASE: "test" 10 | ports: 11 | - "3314:3306" 12 | -------------------------------------------------------------------------------- /examples/mysql/.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /examples/mysql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "mysql" 3 | version = "0.1.0" 4 | edition = "2018" 5 | workspace = "../../" 6 | 7 | [dependencies] 8 | testfixtures = { path = "../../" } 9 | sqlx = { version = "0.3", features = [ "mysql", "chrono" ] } 10 | async-std = { version = "1.5.0", features = [ "attributes" ] } 11 | paw = "1.0" 12 | chrono = "0.4.11" 13 | anyhow = "1.0" 14 | -------------------------------------------------------------------------------- /examples/mysql/README.md: -------------------------------------------------------------------------------- 1 | # MySQL Example 2 | 3 | ## Usage 4 | 5 | ```sh 6 | # setup test db 7 | $ make db 8 | 9 | # run a test 10 | $ DATABASE_URL=mysql://root@127.0.0.1:3314/test cargo test 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/mysql/fixtures/todos.yml: -------------------------------------------------------------------------------- 1 | - id: 1 2 | description: fizz 3 | done: true 4 | progress: 10.5 5 | created_at: 2020/01/01 01:01:01 6 | - id: 2 7 | description: buzz 8 | done: false 9 | progress: 30.0 10 | created_at: 2020/01/01 02:02:02 11 | -------------------------------------------------------------------------------- /examples/mysql/src/main.rs: -------------------------------------------------------------------------------- 1 | use sqlx::MySqlPool; 2 | use std::env; 3 | 4 | #[async_std::main] 5 | #[paw::main] 6 | async fn main() -> anyhow::Result<()> { 7 | let pool = MySqlPool::new(&env::var("DATABASE_URL")?).await?; 8 | println!("{}", list_todos(&pool).await?); 9 | Ok(()) 10 | } 11 | 12 | async fn list_todos(pool: &MySqlPool) -> anyhow::Result { 13 | let recs = sqlx::query!( 14 | r#" 15 | SELECT id, description, done 16 | FROM todos 17 | ORDER BY id 18 | "# 19 | ) 20 | .fetch_all(pool) 21 | .await?; 22 | 23 | let mut todos = "".to_string(); 24 | 25 | for rec in recs { 26 | todos = format!( 27 | "{}{}", 28 | todos, 29 | format!( 30 | "- [{}] {}: {}\n", 31 | if rec.done != 0 { "x" } else { " " }, 32 | rec.id, 33 | &rec.description, 34 | ) 35 | ); 36 | } 37 | 38 | Ok(todos) 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | use chrono::Utc; 45 | use sqlx::MySqlPool; 46 | use std::env; 47 | use testfixtures::MySqlLoader; 48 | 49 | #[async_std::test] 50 | async fn test_list_todos() -> anyhow::Result<()> { 51 | let pool = MySqlPool::new(&env::var("DATABASE_URL")?).await?; 52 | let pool_for_query = pool.clone(); 53 | let loader = MySqlLoader::new(|cfg| { 54 | cfg.location(Utc); 55 | cfg.database(pool); 56 | cfg.skip_test_database_check(); 57 | cfg.paths(vec!["fixtures/todos.yml"]); 58 | }) 59 | .await?; 60 | 61 | // load your fixtures 62 | loader.load().await.unwrap(); 63 | 64 | assert_eq!( 65 | list_todos(&pool_for_query).await?, 66 | "- [x] 1: fizz\n- [ ] 2: buzz\n" 67 | ); 68 | 69 | Ok(()) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /initdb.d/initialize.sql: -------------------------------------------------------------------------------- 1 | create table todos ( 2 | id BIGINT UNSIGNED PRIMARY KEY NOT NULL AUTO_INCREMENT, 3 | description TEXT NOT NULL, 4 | done BOOLEAN NOT NULL DEFAULT FALSE, 5 | progress float, 6 | created_at datetime 7 | ); 8 | 9 | create database if not exists fizz; 10 | -------------------------------------------------------------------------------- /src/fixture_file.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, TimeZone}; 2 | use std::fs::File; 3 | use std::path::Path; 4 | 5 | /// A loaded fixture file. 6 | pub struct FixtureFile { 7 | pub path: String, 8 | pub file_name: String, 9 | pub content: File, 10 | pub insert_sqls: Vec>, 11 | } 12 | 13 | /// SQL query and parameters. 14 | pub struct InsertSql { 15 | pub sql: String, 16 | pub params: Vec>, 17 | } 18 | 19 | /// SQL parameter types. 20 | pub enum SqlParam 21 | where 22 | Tz: TimeZone + Send + Sync, 23 | { 24 | String(String), 25 | Datetime(DateTime), 26 | Integer(u32), 27 | Float(f32), 28 | Boolean(bool), 29 | } 30 | 31 | impl FixtureFile 32 | where 33 | Tz: TimeZone + Send + Sync, 34 | { 35 | pub(crate) fn file_stem(&self) -> String { 36 | Path::new(self.file_name.as_str()) 37 | .file_stem() 38 | .unwrap() 39 | .to_str() 40 | .unwrap() 41 | .to_string() 42 | } 43 | 44 | pub(crate) fn delete(&self) -> String { 45 | format!("DELETE FROM {}", self.file_stem()) 46 | } 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use super::*; 52 | use chrono::Utc; 53 | use std::fs::File; 54 | use std::io::Write; 55 | use tempfile::tempdir; 56 | 57 | #[test] 58 | fn test_file_stem() -> anyhow::Result<()> { 59 | let dir = tempdir()?; 60 | let file_path = dir.path().join("todos.yml"); 61 | let fixture_file_path = file_path.clone(); 62 | let mut file = File::create(file_path)?; 63 | writeln!( 64 | file, 65 | r#" 66 | - id: 1 67 | description: fizz 68 | created_at: 2020/01/01 01:01:01 69 | updated_at: RAW=NOW()"# 70 | ) 71 | .unwrap(); 72 | 73 | let fixture_file = FixtureFile:: { 74 | path: fixture_file_path.to_str().unwrap().to_string(), 75 | file_name: fixture_file_path 76 | .clone() 77 | .file_name() 78 | .unwrap() 79 | .to_str() 80 | .unwrap() 81 | .to_string(), 82 | content: File::open(fixture_file_path.clone()).unwrap(), 83 | insert_sqls: vec![], 84 | }; 85 | 86 | assert_eq!(fixture_file.file_stem(), "todos"); 87 | Ok(()) 88 | } 89 | 90 | #[test] 91 | fn test_delete() -> anyhow::Result<()> { 92 | let dir = tempdir()?; 93 | let file_path = dir.path().join("todos.yml"); 94 | let fixture_file_path = file_path.clone(); 95 | let mut file = File::create(file_path)?; 96 | writeln!( 97 | file, 98 | r#" 99 | - id: 1 100 | description: fizz 101 | created_at: 2020/01/01 01:01:01 102 | updated_at: RAW=NOW()"# 103 | ) 104 | .unwrap(); 105 | 106 | let fixture_file = FixtureFile:: { 107 | path: fixture_file_path.to_str().unwrap().to_string(), 108 | file_name: fixture_file_path 109 | .clone() 110 | .file_name() 111 | .unwrap() 112 | .to_str() 113 | .unwrap() 114 | .to_string(), 115 | content: File::open(fixture_file_path.clone()).unwrap(), 116 | insert_sqls: vec![], 117 | }; 118 | 119 | assert_eq!(fixture_file.delete(), "DELETE FROM todos"); 120 | Ok(()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/helper.rs: -------------------------------------------------------------------------------- 1 | use crate::fixture_file::FixtureFile; 2 | use async_trait::async_trait; 3 | use chrono::{Offset, TimeZone}; 4 | use sqlx::{Connect, Connection, Database as DB, Pool}; 5 | 6 | /// Represents a type that execute SQL queries. 7 | #[async_trait] 8 | pub trait Database 9 | where 10 | D: DB + Sync + Send, 11 | C: Connection + Connect + Sync + Send, 12 | O: Offset + Sync + Send, 13 | Tz: TimeZone + Send + Sync, 14 | { 15 | /// Initialize Database struct. 16 | async fn init(&mut self, db: &Pool) -> anyhow::Result<()>; 17 | 18 | /// Get database name by excuting SQL query. 19 | async fn database_name(&self, db: &Pool) -> anyhow::Result; 20 | 21 | // TODO: complete this function 22 | // async fn table_names(&self, db: &Pool) -> anyhow::Result>; 23 | 24 | /// Execute SQL queries in a transaction. 25 | async fn with_transaction( 26 | &self, 27 | pool: &Pool, 28 | fixture_files: &[FixtureFile], 29 | ) -> anyhow::Result<()>; 30 | } 31 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! [![github]](https://github.com/TaKO8Ki/testfixtures) 2 | //! 3 | //! [github]: https://img.shields.io/badge/github-8da0cb?labelColor=555555&logo=github 4 | //! 5 | //! This crate is a Rust library for preparing test data from yaml files. 6 | //! 7 | //! ## Examples 8 | //! `todos.yml` 9 | //! ```yml 10 | //! - id: 1 11 | //! description: buy a new camera 12 | //! done: true 13 | //! progress: 10.5 14 | //! created_at: 2020/01/01 01:01:01 15 | //! - id: 2 16 | //! description: meeting 17 | //! done: false 18 | //! progress: 30.0 19 | //! created_at: 2020/01/01 02:02:02 20 | //! ``` 21 | //! 22 | //! ```rust 23 | //! #[cfg(test)] 24 | //! mod tests { 25 | //! use chrono::Utc; 26 | //! use sqlx::MySqlPool; 27 | //! use std::env; 28 | //! use testfixtures::MySqlLoader; 29 | //! #[async_std::test] 30 | //! async fn test_something() -> anyhow::Result<()> { 31 | //! let pool = MySqlPool::new(&env::var("DATABASE_URL")?).await?; 32 | //! let loader = MySqlLoader::new(|cfg| { 33 | //! cfg.location(Utc); 34 | //! cfg.database(pool); 35 | //! cfg.skip_test_database_check(); 36 | //! cfg.paths(vec!["fixtures/todos.yml"]); 37 | //! }) 38 | //! .await?; 39 | //! 40 | //! // load your fixtures 41 | //! loader.load().await.unwrap(); 42 | //! 43 | //! // run your tests 44 | //! Ok(()) 45 | //! } 46 | //! } 47 | //! ``` 48 | 49 | mod fixture_file; 50 | mod helper; 51 | mod loader; 52 | mod mysql; 53 | 54 | pub use fixture_file::{FixtureFile, InsertSql, SqlParam}; 55 | pub use helper::Database; 56 | pub use loader::Loader; 57 | pub use mysql::helper::MySql; 58 | pub use mysql::loader::MySqlLoader; 59 | -------------------------------------------------------------------------------- /src/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::fixture_file::{FixtureFile, InsertSql, SqlParam}; 2 | use crate::helper::Database as DB; 3 | use chrono::{DateTime, Offset, TimeZone}; 4 | use regex::Regex; 5 | use sqlx::{Connect, Connection, Database, Pool}; 6 | use std::fmt::Display; 7 | use std::fs::{self, File}; 8 | use std::io::prelude::*; 9 | use std::io::BufReader; 10 | use std::path::Path; 11 | use std::str::FromStr; 12 | use yaml_rust::{Yaml, YamlLoader}; 13 | 14 | /// This type accepts and set some options. 15 | pub struct Loader 16 | where 17 | D: Database + Sync + Send, 18 | C: Connection + Connect + Sync + Send, 19 | O: Offset, 20 | Tz: TimeZone + Send + Sync, 21 | { 22 | pub pool: Option>, 23 | pub helper: Option>>, 24 | pub fixture_files: Vec>, 25 | pub skip_test_database_check: bool, 26 | pub location: Option, 27 | } 28 | 29 | impl Default for Loader 30 | where 31 | D: Database + Sync + Send, 32 | C: Connection + Connect + Sync + Send, 33 | O: Offset, 34 | Tz: TimeZone + Send + Sync, 35 | { 36 | fn default() -> Self { 37 | Loader:: { 38 | pool: None, 39 | helper: None, 40 | fixture_files: vec![], 41 | skip_test_database_check: false, 42 | location: None, 43 | } 44 | } 45 | } 46 | 47 | impl Loader 48 | where 49 | D: Database + Sync + Send, 50 | C: Connection + Connect + Sync + Send, 51 | O: Offset + Display + Send + Sync, 52 | Tz: TimeZone + Send + Sync, 53 | { 54 | /// Execute SQL queries builded from yaml files. 55 | pub async fn load(&self) -> anyhow::Result<()> { 56 | if !self.skip_test_database_check { 57 | if let Err(err) = self.ensure_test_database().await { 58 | return Err(anyhow::anyhow!("testfixtures: {}", err)); 59 | } 60 | } 61 | 62 | self.helper 63 | .as_ref() 64 | .unwrap() 65 | .with_transaction(self.pool.as_ref().unwrap(), &self.fixture_files) 66 | .await?; 67 | Ok(()) 68 | } 69 | 70 | /// Set database pool. 71 | pub fn database(&mut self, pool: Pool) { 72 | self.pool = Some(pool) 73 | } 74 | 75 | /// Turn test database check off. 76 | pub fn skip_test_database_check(&mut self) { 77 | self.skip_test_database_check = true 78 | } 79 | 80 | /// Set timezone. 81 | pub fn location(&mut self, location: Tz) { 82 | self.location = Some(location) 83 | } 84 | 85 | /// Set fixture files directly. 86 | pub fn files(&mut self, files: Vec<&str>) { 87 | let mut fixtures = Self::fixtures_from_files(files); 88 | self.fixture_files.append(&mut fixtures) 89 | } 90 | 91 | /// Set fixture files from a directory. 92 | pub fn directory(&mut self, directory: &str) { 93 | let mut fixtures = Self::fixtures_from_directory(directory); 94 | self.fixture_files.append(&mut fixtures) 95 | } 96 | 97 | /// This option is a combination of files option and directory option. 98 | pub fn paths(&mut self, paths: Vec<&str>) { 99 | let mut fixtures = Self::fixtures_from_paths(paths); 100 | self.fixture_files.append(&mut fixtures) 101 | } 102 | 103 | /// Try change str to datetime. 104 | fn try_str_to_date(&self, s: String) -> anyhow::Result> { 105 | let formats = vec![ 106 | "%Y-%m-%d %H:%M", 107 | "%Y-%m-%d %H:%M:%S", 108 | "%Y%m%d %H:%M", 109 | "%Y%m%d %H:%M:%S", 110 | "%d%m%Y %H:%M", 111 | "%d%m%Y %H:%M:%S", 112 | "%Y/%m/%d %H:%M", 113 | "%Y/%m/%d %H:%M:%S", 114 | ]; 115 | for f in formats { 116 | let result = self 117 | .location 118 | .as_ref() 119 | .unwrap() 120 | .datetime_from_str(s.as_str(), f); 121 | if let Ok(datetime) = result { 122 | return Ok(datetime); 123 | } 124 | } 125 | Err(anyhow::anyhow!("testfixtures: '{}' is invalid format", s)) 126 | } 127 | 128 | /// Set fixture file content to FixtureFile struct. 129 | fn fixtures_from_files(files: Vec<&str>) -> Vec> { 130 | let mut fixture_files: Vec> = vec![]; 131 | for f in files { 132 | let fixture = FixtureFile { 133 | path: f.to_string(), 134 | file_name: Path::new(f) 135 | .file_name() 136 | .unwrap() 137 | .to_str() 138 | .unwrap() 139 | .to_string(), 140 | content: File::open(f).unwrap(), 141 | insert_sqls: vec![], 142 | }; 143 | fixture_files.push(fixture); 144 | } 145 | fixture_files 146 | } 147 | 148 | /// Set fixture file content from a directory to [FixtureFile](crate::fixture_file::FixtureFile) struct. 149 | fn fixtures_from_directory(directory: &str) -> Vec> { 150 | let mut fixture_files: Vec> = vec![]; 151 | for f in fs::read_dir(directory).unwrap() { 152 | let f = f.unwrap(); 153 | let file_extension = match f.path().extension() { 154 | Some(ext) => ext.to_str().unwrap().to_string(), 155 | None => "".to_string(), 156 | }; 157 | if !f.path().is_dir() && (file_extension == "yml" || file_extension == "yaml") { 158 | let fixture = FixtureFile { 159 | path: f.path().to_str().unwrap().to_string(), 160 | file_name: f.file_name().to_str().unwrap().to_string(), 161 | content: File::open(f.path()).unwrap(), 162 | insert_sqls: vec![], 163 | }; 164 | fixture_files.push(fixture); 165 | } 166 | } 167 | fixture_files 168 | } 169 | 170 | /// Set fixture file content from a directory to [FixtureFile](crate::fixture_file::FixtureFile) struct. 171 | fn fixtures_from_paths(paths: Vec<&str>) -> Vec> { 172 | let mut fixture_files: Vec> = vec![]; 173 | for path in paths { 174 | if Path::new(path).is_dir() { 175 | fixture_files.append(&mut Self::fixtures_from_directory(path)) 176 | } else { 177 | fixture_files.append(&mut Self::fixtures_from_files(vec![path])) 178 | } 179 | } 180 | fixture_files 181 | } 182 | 183 | /// Build SQL queries from fixture files. 184 | pub(crate) fn build_insert_sqls(&mut self) { 185 | for index in 0..self.fixture_files.len() { 186 | let file = &self.fixture_files[index].content; 187 | let mut buf_reader = BufReader::new(file); 188 | let mut content = String::new(); 189 | buf_reader.read_to_string(&mut content).unwrap(); 190 | let records = YamlLoader::load_from_str(content.as_str()).unwrap(); 191 | 192 | if let Yaml::Array(records) = &records[0] { 193 | for record in records { 194 | let (sql, values) = self.build_insert_sql(&self.fixture_files[index], record); 195 | self.fixture_files[index].insert_sqls.push(InsertSql { 196 | sql, 197 | params: values, 198 | }); 199 | } 200 | }; 201 | } 202 | } 203 | 204 | fn build_insert_sql( 205 | &self, 206 | file: &FixtureFile, 207 | record: &Yaml, 208 | ) -> (String, Vec>) { 209 | let mut sql_columns = vec![]; 210 | let mut sql_values = vec![]; 211 | let mut values = vec![]; 212 | if let Yaml::Hash(hash) = &record { 213 | for (key, value) in hash { 214 | match key { 215 | Yaml::String(k) => sql_columns.push(k.to_string()), 216 | Yaml::Integer(k) => sql_columns.push(k.to_string()), 217 | _ => (), 218 | }; 219 | match value { 220 | Yaml::String(v) => { 221 | if v.starts_with("RAW=") { 222 | sql_values.push(v.replace("RAW=", "")); 223 | continue; 224 | } else { 225 | match self.try_str_to_date(v.to_string()) { 226 | Ok(datetime) => values.push(SqlParam::Datetime(datetime)), 227 | Err(_) => values.push(SqlParam::String(v.to_string())), 228 | } 229 | } 230 | } 231 | Yaml::Integer(v) => values.push(SqlParam::Integer(*v as u32)), 232 | Yaml::Real(v) => values.push(SqlParam::Float(f32::from_str(v).unwrap())), 233 | Yaml::Boolean(v) => values.push(SqlParam::Boolean(*v)), 234 | _ => (), 235 | }; 236 | sql_values.push("?".to_string()); 237 | } 238 | }; 239 | 240 | let sql_str = format!( 241 | "INSERT INTO {} ({}) VALUES ({})", 242 | file.file_stem(), 243 | sql_columns.join(", "), 244 | sql_values.join(", "), 245 | ); 246 | (sql_str, values) 247 | } 248 | 249 | // Check if database name ends with test. 250 | async fn ensure_test_database(&self) -> anyhow::Result<()> { 251 | let db_name = self 252 | .helper 253 | .as_ref() 254 | .unwrap() 255 | .database_name(self.pool.as_ref().unwrap()) 256 | .await?; 257 | let re = Regex::new(r"^*?test$")?; 258 | if !re.is_match(db_name.as_str()) { 259 | return Err(anyhow::anyhow!( 260 | r#"'{}' does not appear to be a test database"#, 261 | db_name 262 | )); 263 | } 264 | Ok(()) 265 | } 266 | } 267 | 268 | #[cfg(test)] 269 | mod tests { 270 | use crate::fixture_file::{FixtureFile, SqlParam}; 271 | use crate::helper::Database as DB; 272 | use crate::mysql::loader::MySqlLoader; 273 | use async_trait::async_trait; 274 | use chrono::{prelude::*, Utc}; 275 | use sqlx::{MySql as M, MySqlConnection, MySqlPool}; 276 | use std::fs::File; 277 | use std::io::{prelude::*, BufReader, Write}; 278 | use tempfile::{tempdir, TempDir}; 279 | use yaml_rust::{Yaml, YamlLoader}; 280 | 281 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 282 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 283 | async fn it_returns_ok() -> anyhow::Result<()> { 284 | pub struct TestLoadNormal {} 285 | impl Default for TestLoadNormal { 286 | fn default() -> Self { 287 | TestLoadNormal {} 288 | } 289 | } 290 | #[async_trait] 291 | impl DB for TestLoadNormal 292 | where 293 | O: Offset + Sync + Send + 'static, 294 | Tz: TimeZone + Send + Sync + 'static, 295 | { 296 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 297 | Ok(()) 298 | } 299 | 300 | async fn database_name(&self, _pool: &MySqlPool) -> anyhow::Result { 301 | Ok("test".to_string()) 302 | } 303 | 304 | async fn with_transaction( 305 | &self, 306 | _pool: &MySqlPool, 307 | _fixture_files: &[FixtureFile], 308 | ) -> anyhow::Result<()> { 309 | Ok(()) 310 | } 311 | } 312 | 313 | let mut loader = MySqlLoader::::default(); 314 | loader.pool = Some(MySqlPool::new("fizz").await?); 315 | loader.helper = Some(Box::new(TestLoadNormal {})); 316 | let result = loader.load().await; 317 | assert!(result.is_ok()); 318 | Ok(()) 319 | } 320 | 321 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 322 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 323 | async fn it_returns_transaction_error() -> anyhow::Result<()> { 324 | pub struct TestLoadTransactionError {} 325 | impl Default for TestLoadTransactionError { 326 | fn default() -> Self { 327 | TestLoadTransactionError {} 328 | } 329 | } 330 | #[async_trait] 331 | impl DB for TestLoadTransactionError 332 | where 333 | O: Offset + Sync + Send + 'static, 334 | Tz: TimeZone + Send + Sync + 'static, 335 | { 336 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 337 | Ok(()) 338 | } 339 | 340 | async fn database_name(&self, _pool: &MySqlPool) -> anyhow::Result { 341 | Ok("test".to_string()) 342 | } 343 | 344 | async fn with_transaction( 345 | &self, 346 | _pool: &MySqlPool, 347 | _fixture_files: &[FixtureFile], 348 | ) -> anyhow::Result<()> { 349 | Err(anyhow::anyhow!("error")) 350 | } 351 | } 352 | 353 | let mut loader = MySqlLoader::::default(); 354 | loader.pool = Some(MySqlPool::new("fizz").await?); 355 | loader.helper = Some(Box::new(TestLoadTransactionError {})); 356 | let result = loader.load().await; 357 | assert!(result.is_err()); 358 | if let Err(err) = result { 359 | assert_eq!(err.to_string(), "error"); 360 | } 361 | Ok(()) 362 | } 363 | 364 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 365 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 366 | async fn it_returns_dabatase_check_error() -> anyhow::Result<()> { 367 | pub struct TestLoadDatabaseCheckError {} 368 | impl Default for TestLoadDatabaseCheckError { 369 | fn default() -> Self { 370 | TestLoadDatabaseCheckError {} 371 | } 372 | } 373 | #[async_trait] 374 | impl DB for TestLoadDatabaseCheckError 375 | where 376 | O: Offset + Sync + Send + 'static, 377 | Tz: TimeZone + Send + Sync + 'static, 378 | { 379 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 380 | Ok(()) 381 | } 382 | 383 | async fn database_name(&self, _pool: &MySqlPool) -> anyhow::Result { 384 | Ok("fizz".to_string()) 385 | } 386 | 387 | async fn with_transaction( 388 | &self, 389 | _pool: &MySqlPool, 390 | _fixture_files: &[FixtureFile], 391 | ) -> anyhow::Result<()> { 392 | Ok(()) 393 | } 394 | } 395 | 396 | let mut loader = MySqlLoader::::default(); 397 | loader.pool = Some(MySqlPool::new("fizz").await?); 398 | loader.helper = Some(Box::new(TestLoadDatabaseCheckError {})); 399 | let result = loader.load().await; 400 | assert!(result.is_err()); 401 | if let Err(err) = result { 402 | assert_eq!( 403 | err.to_string(), 404 | r#"testfixtures: 'fizz' does not appear to be a test database"# 405 | ); 406 | } 407 | Ok(()) 408 | } 409 | 410 | #[test] 411 | fn test_location() { 412 | let mut loader = MySqlLoader::::default(); 413 | loader.location(Utc); 414 | assert_eq!(loader.location.unwrap(), Utc); 415 | } 416 | 417 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 418 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 419 | async fn test_database() -> anyhow::Result<()> { 420 | let mut loader = MySqlLoader::::default(); 421 | let database = MySqlPool::new("fizz").await?; 422 | loader.database(database); 423 | assert!(loader.pool.is_some()); 424 | Ok(()) 425 | } 426 | 427 | #[test] 428 | fn test_skip_test_database_check() { 429 | let mut loader = MySqlLoader::::default(); 430 | loader.skip_test_database_check(); 431 | assert!(loader.skip_test_database_check); 432 | } 433 | 434 | #[test] 435 | fn test_files() { 436 | let dir = tempdir().unwrap(); 437 | let file_path = dir.path().join("todos.yml"); 438 | let fixture_file_path = file_path.clone(); 439 | let mut file = File::create(file_path).unwrap(); 440 | writeln!( 441 | file, 442 | r#" 443 | - id: 1 444 | description: fizz 445 | created_at: 2020/01/01 01:01:01 446 | updated_at: RAW=NOW()"# 447 | ) 448 | .unwrap(); 449 | let mut loader = MySqlLoader::::default(); 450 | loader.files(vec![fixture_file_path.to_str().unwrap()]); 451 | assert_eq!( 452 | loader.fixture_files[0].file_name, 453 | fixture_file_path.file_name().unwrap().to_str().unwrap() 454 | ); 455 | } 456 | 457 | #[test] 458 | fn test_directory() -> anyhow::Result<()> { 459 | let dir = tempdir()?; 460 | let file_path = dir.path().join("todos.yml"); 461 | let mut file = File::create(file_path)?; 462 | writeln!( 463 | file, 464 | r#" 465 | - id: 1 466 | description: fizz 467 | created_at: 2020/01/01 01:01:01 468 | updated_at: RAW=NOW()"# 469 | ) 470 | .unwrap(); 471 | let mut loader = MySqlLoader::::default(); 472 | loader.directory(dir.path().to_str().unwrap()); 473 | assert_eq!(loader.fixture_files[0].file_name, "todos.yml"); 474 | Ok(()) 475 | } 476 | 477 | #[test] 478 | fn test_paths() -> anyhow::Result<()> { 479 | let dir = tempdir()?; 480 | let file_1_path = dir.path().join("test_1.yml"); 481 | let mut file_1 = File::create(file_1_path)?; 482 | let file_2_path = dir.path().join("test_2.yml"); 483 | let mut file_2 = File::create(file_2_path)?; 484 | writeln!( 485 | file_1, 486 | r#" 487 | - id: 1 488 | description: fizz 489 | created_at: 2020/01/01 01:01:01 490 | updated_at: RAW=NOW()"# 491 | ) 492 | .unwrap(); 493 | writeln!( 494 | file_2, 495 | r#" 496 | - id: 1 497 | description: fizz 498 | created_at: 2020/01/01 01:01:01 499 | updated_at: RAW=NOW()"# 500 | ) 501 | .unwrap(); 502 | let mut loader = MySqlLoader::::default(); 503 | loader.paths(vec![ 504 | dir.path().to_str().unwrap(), 505 | format!( 506 | "{}/{}", 507 | dir.path().to_str().unwrap(), 508 | dir.path() 509 | .join("test_1.yml") 510 | .file_name() 511 | .unwrap() 512 | .to_str() 513 | .unwrap() 514 | ) 515 | .as_str(), 516 | ]); 517 | Ok(()) 518 | } 519 | 520 | #[test] 521 | fn test_try_str_to_date() { 522 | struct Test { 523 | argument: String, 524 | want_err: bool, 525 | }; 526 | let tests: [Test; 10] = [ 527 | Test { 528 | argument: "2020-01-01 01:01:01".to_string(), 529 | want_err: false, 530 | }, 531 | Test { 532 | argument: "2020-01-01 01:01".to_string(), 533 | want_err: false, 534 | }, 535 | Test { 536 | argument: "2020/01/01 01:01:01".to_string(), 537 | want_err: false, 538 | }, 539 | Test { 540 | argument: "2020/01/01 01:01".to_string(), 541 | want_err: false, 542 | }, 543 | Test { 544 | argument: "01012020 01:01:01".to_string(), 545 | want_err: false, 546 | }, 547 | Test { 548 | argument: "01012020 01:01".to_string(), 549 | want_err: false, 550 | }, 551 | Test { 552 | argument: "2020-01-01".to_string(), 553 | want_err: true, 554 | }, 555 | Test { 556 | argument: "2020/01/01".to_string(), 557 | want_err: true, 558 | }, 559 | Test { 560 | argument: "01012020".to_string(), 561 | want_err: true, 562 | }, 563 | Test { 564 | argument: "fizzbuzz".to_string(), 565 | want_err: true, 566 | }, 567 | ]; 568 | let mut loader = MySqlLoader::::default(); 569 | loader.location(Utc); 570 | for t in &tests { 571 | if t.want_err { 572 | assert!(loader.try_str_to_date(t.argument.to_string()).is_err()); 573 | if let Err(err) = loader.try_str_to_date(t.argument.to_string()) { 574 | assert_eq!( 575 | err.to_string(), 576 | format!("testfixtures: '{}' is invalid format", t.argument) 577 | ) 578 | } 579 | } else { 580 | assert!(loader.try_str_to_date(t.argument.to_string()).is_ok()); 581 | } 582 | } 583 | } 584 | 585 | #[test] 586 | fn test_fixtures_from_files() { 587 | let dir = tempdir().unwrap(); 588 | let file_path = dir.path().join("todos.yml"); 589 | let fixture_file_path = file_path.clone(); 590 | let mut file = File::create(file_path).unwrap(); 591 | writeln!( 592 | file, 593 | r#" 594 | - id: 1 595 | description: fizz 596 | created_at: 2020/01/01 01:01:01 597 | updated_at: RAW=NOW()"# 598 | ) 599 | .unwrap(); 600 | let fixture_files = 601 | MySqlLoader::::fixtures_from_files(vec![fixture_file_path.to_str().unwrap()]); 602 | assert_eq!( 603 | fixture_files[0].file_name, 604 | fixture_file_path.file_name().unwrap().to_str().unwrap() 605 | ); 606 | } 607 | 608 | #[test] 609 | fn test_fixtures_from_directory() -> anyhow::Result<()> { 610 | let dir = tempdir()?; 611 | TempDir::new_in(dir.path())?; 612 | let file_path = dir.path().join("todos.yml"); 613 | let text_file_path = dir.path().join("test.txt"); 614 | let mut file = File::create(file_path)?; 615 | File::create(text_file_path)?; 616 | writeln!( 617 | file, 618 | r#" 619 | - id: 1 620 | description: fizz 621 | created_at: 2020/01/01 01:01:01 622 | updated_at: RAW=NOW()"# 623 | ) 624 | .unwrap(); 625 | let fixture_files = 626 | MySqlLoader::::fixtures_from_directory(dir.path().to_str().unwrap()); 627 | assert_eq!(fixture_files.len(), 1); 628 | assert_eq!(fixture_files[0].file_name, "todos.yml"); 629 | Ok(()) 630 | } 631 | 632 | #[test] 633 | fn test_fixtures_from_paths() -> anyhow::Result<()> { 634 | let dir = tempdir()?; 635 | let file_1_path = dir.path().join("test_1.yml"); 636 | let mut file_1 = File::create(file_1_path)?; 637 | let file_2_path = dir.path().join("test_2.yml"); 638 | let mut file_2 = File::create(file_2_path)?; 639 | writeln!( 640 | file_1, 641 | r#" 642 | - id: 1 643 | description: fizz 644 | created_at: 2020/01/01 01:01:01 645 | updated_at: RAW=NOW()"# 646 | ) 647 | .unwrap(); 648 | writeln!( 649 | file_2, 650 | r#" 651 | - id: 1 652 | description: fizz 653 | created_at: 2020/01/01 01:01:01 654 | updated_at: RAW=NOW()"# 655 | ) 656 | .unwrap(); 657 | let fixture_files = MySqlLoader::::fixtures_from_paths(vec![ 658 | dir.path().to_str().unwrap(), 659 | dir.path().join("test_2.yml").to_str().unwrap(), 660 | ]); 661 | assert_eq!(fixture_files[0].file_name, "test_2.yml"); 662 | assert_eq!( 663 | fixture_files[1].file_name, 664 | dir.path() 665 | .join("test_1.yml") 666 | .file_name() 667 | .unwrap() 668 | .to_str() 669 | .unwrap() 670 | ); 671 | Ok(()) 672 | } 673 | 674 | #[test] 675 | fn test_build_insert_sql() { 676 | // different columns have different types. 677 | let dir = tempdir().unwrap(); 678 | let file_path = dir.path().join("todos.yml"); 679 | let fixture_file_path = file_path.clone(); 680 | let mut file = File::create(file_path).unwrap(); 681 | writeln!( 682 | file, 683 | r#" 684 | - id: 1 685 | description: fizz 686 | price: 1.1 687 | created_at: 2020/01/01 01:01:01 688 | updated_at: RAW=NOW()"# 689 | ) 690 | .unwrap(); 691 | 692 | let mut loader = MySqlLoader::::default(); 693 | loader.location(Utc); 694 | let fixture_file = FixtureFile { 695 | path: fixture_file_path.to_str().unwrap().to_string(), 696 | file_name: fixture_file_path 697 | .clone() 698 | .file_name() 699 | .unwrap() 700 | .to_str() 701 | .unwrap() 702 | .to_string(), 703 | content: File::open(fixture_file_path).unwrap(), 704 | insert_sqls: vec![], 705 | }; 706 | let mut buf_reader = BufReader::new(&fixture_file.content); 707 | let mut contents = String::new(); 708 | buf_reader.read_to_string(&mut contents).unwrap(); 709 | let records = YamlLoader::load_from_str(contents.as_str()).unwrap(); 710 | if let Yaml::Array(records) = &records[0] { 711 | let (sql_str, values) = loader.build_insert_sql(&fixture_file, &records[0]); 712 | assert_eq!(sql_str, format!("INSERT INTO {} (id, description, price, created_at, updated_at) VALUES (?, ?, ?, ?, NOW())", fixture_file.file_stem())); 713 | assert_eq!(values.len(), 4); 714 | if let SqlParam::Integer(param) = &values[0] { 715 | assert_eq!(*param, 1) 716 | } 717 | if let SqlParam::String(param) = &values[1] { 718 | assert_eq!(*param, "fizz".to_string()) 719 | } 720 | if let SqlParam::Float(param) = &values[2] { 721 | assert_eq!(*param, 1.1) 722 | } 723 | if let SqlParam::Datetime(param) = &values[3] { 724 | assert_eq!(*param, Utc.ymd(2020, 1, 1).and_hms(1, 1, 1)) 725 | } 726 | } 727 | } 728 | 729 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 730 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 731 | async fn test_ensure_test_database() -> anyhow::Result<()> { 732 | pub struct TestEnsureTestDatabaseNormal {} 733 | impl Default for TestEnsureTestDatabaseNormal { 734 | fn default() -> Self { 735 | TestEnsureTestDatabaseNormal {} 736 | } 737 | } 738 | #[async_trait] 739 | impl DB for TestEnsureTestDatabaseNormal 740 | where 741 | O: Offset + Sync + Send + 'static, 742 | Tz: TimeZone + Send + Sync + 'static, 743 | { 744 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 745 | Ok(()) 746 | } 747 | 748 | async fn database_name(&self, _pool: &MySqlPool) -> anyhow::Result { 749 | Ok("test".to_string()) 750 | } 751 | 752 | async fn with_transaction( 753 | &self, 754 | _pool: &MySqlPool, 755 | _fixture_files: &[FixtureFile], 756 | ) -> anyhow::Result<()> { 757 | Ok(()) 758 | } 759 | } 760 | let mut loader = MySqlLoader::::default(); 761 | loader.pool = Some(MySqlPool::new("fizz").await?); 762 | loader.helper = Some(Box::new(TestEnsureTestDatabaseNormal {})); 763 | let result = loader.ensure_test_database().await?; 764 | assert_eq!(result, ()); 765 | 766 | pub struct TestEnsureTestDatabaseError {} 767 | impl Default for TestEnsureTestDatabaseError { 768 | fn default() -> Self { 769 | TestEnsureTestDatabaseError {} 770 | } 771 | } 772 | #[async_trait] 773 | impl DB for TestEnsureTestDatabaseError 774 | where 775 | O: Offset + Sync + Send + 'static, 776 | Tz: TimeZone + Send + Sync + 'static, 777 | { 778 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 779 | Ok(()) 780 | } 781 | 782 | async fn database_name(&self, _pool: &MySqlPool) -> anyhow::Result { 783 | Ok("fizz".to_string()) 784 | } 785 | 786 | async fn with_transaction( 787 | &self, 788 | _pool: &MySqlPool, 789 | _fixture_files: &[FixtureFile], 790 | ) -> anyhow::Result<()> { 791 | Ok(()) 792 | } 793 | } 794 | let mut loader = MySqlLoader::::default(); 795 | loader.pool = Some(MySqlPool::new("fizz").await?); 796 | loader.helper = Some(Box::new(TestEnsureTestDatabaseError {})); 797 | let result = loader.ensure_test_database().await; 798 | assert!(result.is_err()); 799 | 800 | Ok(()) 801 | } 802 | } 803 | -------------------------------------------------------------------------------- /src/mysql/helper.rs: -------------------------------------------------------------------------------- 1 | use crate::fixture_file::{FixtureFile, SqlParam}; 2 | use crate::helper::Database as DB; 3 | use async_trait::async_trait; 4 | use chrono::{Offset, TimeZone}; 5 | use sqlx::mysql::MySqlQueryAs; 6 | use sqlx::{ 7 | arguments::Arguments, mysql::MySqlArguments, Error, MySql as M, MySqlConnection, MySqlPool, 8 | Query, 9 | }; 10 | 11 | /// **MySQL** helper. 12 | pub struct MySql { 13 | pub table_names: Vec, 14 | } 15 | 16 | impl Default for MySql { 17 | fn default() -> Self { 18 | MySql { 19 | table_names: vec![], 20 | } 21 | } 22 | } 23 | 24 | #[async_trait] 25 | impl DB for MySql 26 | where 27 | O: Offset + Sync + Send + 'static, 28 | Tz: TimeZone + Send + Sync + 'static, 29 | { 30 | /// Initialize MySQL struct. 31 | async fn init(&mut self, _pool: &MySqlPool) -> anyhow::Result<()> { 32 | Ok(()) 33 | } 34 | 35 | /// Get database name. 36 | async fn database_name(&self, pool: &MySqlPool) -> anyhow::Result { 37 | let rec: (String,) = sqlx::query_as("SELECT DATABASE()").fetch_one(pool).await?; 38 | Ok(rec.0) 39 | } 40 | 41 | // TODO: complete this function 42 | // async fn table_names(&self, pool: &MySqlPool) -> anyhow::Result> { 43 | // let tables = sqlx::query_as( 44 | // r#" 45 | // SELECT table_name 46 | // FROM information_schema.tables 47 | // WHERE table_schema = ? AND table_type = 'BASE TABLE'; 48 | // "#, 49 | // ) 50 | // .bind("test") 51 | // .fetch_all(pool) 52 | // .await?; 53 | 54 | // let mut names = vec![]; 55 | // for table in tables { 56 | // names.push(table.table_name) 57 | // } 58 | // Ok(names) 59 | // } 60 | 61 | /// Execute SQL queries in a transaction for MySQL. 62 | async fn with_transaction( 63 | &self, 64 | pool: &MySqlPool, 65 | fixture_files: &[FixtureFile], 66 | ) -> anyhow::Result<()> { 67 | let mut tx = pool.begin().await?; 68 | 69 | let mut queries = vec![]; 70 | let delete_queries: Vec = fixture_files.iter().map(|x| (x.delete())).collect(); 71 | let mut delete_queries: Vec> = 72 | delete_queries.iter().map(|x| sqlx::query(x)).collect(); 73 | queries.append(&mut delete_queries); 74 | 75 | for fixtures_file in fixture_files { 76 | for sql in &fixtures_file.insert_sqls { 77 | let mut args = MySqlArguments::default(); 78 | for param in &sql.params { 79 | match param { 80 | SqlParam::String(param) => args.add(param), 81 | SqlParam::Integer(param) => args.add(param), 82 | SqlParam::Datetime(param) => args.add(param.naive_local()), 83 | SqlParam::Float(param) => args.add(param), 84 | SqlParam::Boolean(param) => args.add(param), 85 | } 86 | } 87 | queries.push(sqlx::query(sql.sql.as_str()).bind_all(args)) 88 | } 89 | } 90 | 91 | let result: Result = async { 92 | sqlx::query("SET FOREIGN_KEY_CHECKS = 0") 93 | .execute(&mut tx) 94 | .await?; 95 | 96 | for query in queries { 97 | let result = query.execute(&mut tx).await; 98 | if result.is_err() { 99 | return result; 100 | } 101 | } 102 | 103 | sqlx::query("SET FOREIGN_KEY_CHECKS = 1") 104 | .execute(&mut tx) 105 | .await?; 106 | Ok(1) 107 | } 108 | .await; 109 | 110 | match result { 111 | Ok(_) => { 112 | tx.commit().await?; 113 | } 114 | Err(err) => { 115 | tx.rollback().await?; 116 | return Err(anyhow::anyhow!("testfixtures: {}", err)); 117 | } 118 | }; 119 | Ok(()) 120 | } 121 | } 122 | 123 | #[cfg(test)] 124 | #[cfg(feature = "mysql")] 125 | mod tests { 126 | use crate::fixture_file::FixtureFile; 127 | use crate::mysql::helper::MySql; 128 | use crate::mysql::loader::MySqlLoader; 129 | use chrono::{prelude::*, NaiveDate, Utc}; 130 | use sqlx::{cursor::Cursor, MySqlPool, Row}; 131 | use std::env; 132 | use std::fs::File; 133 | use std::io::Write; 134 | use tempfile::tempdir; 135 | 136 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 137 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 138 | async fn test_with_transaction() -> anyhow::Result<()> { 139 | let pool = MySqlPool::new(&env::var("TEST_DB_URL")?).await?; 140 | let dir = tempdir()?; 141 | let file_path = dir.path().join("todos.yml"); 142 | let fixture_file_path = file_path.clone(); 143 | let mut file = File::create(file_path)?; 144 | writeln!( 145 | file, 146 | r#" 147 | - id: 1 148 | description: fizz 149 | done: false 150 | progress: 10.5 151 | created_at: 2020/01/01 01:01:01"# 152 | ) 153 | .unwrap(); 154 | 155 | let mut loader = MySqlLoader::::default(); 156 | loader.location(Utc); 157 | loader.helper = Some(Box::new(MySql::default())); 158 | let fixture_file = FixtureFile:: { 159 | path: fixture_file_path.to_str().unwrap().to_string(), 160 | file_name: fixture_file_path 161 | .clone() 162 | .file_name() 163 | .unwrap() 164 | .to_str() 165 | .unwrap() 166 | .to_string(), 167 | content: File::open(fixture_file_path).unwrap(), 168 | insert_sqls: vec![], 169 | }; 170 | loader.fixture_files = vec![fixture_file]; 171 | loader.build_insert_sqls(); 172 | let result = loader 173 | .helper 174 | .unwrap() 175 | .with_transaction(&pool, &loader.fixture_files) 176 | .await; 177 | 178 | if let Err(err) = result { 179 | panic!("test error: {}", err) 180 | }; 181 | 182 | let mut cursor = 183 | sqlx::query("SELECT id, description, done, progress, created_at FROM todos") 184 | .fetch(&pool); 185 | let row = cursor.next().await?.unwrap(); 186 | let id: u16 = row.get("id"); 187 | let description: String = row.get("description"); 188 | let done: bool = row.get("done"); 189 | let progress: f32 = row.get("progress"); 190 | let created_at: NaiveDateTime = row.get("created_at"); 191 | assert_eq!(id, 1); 192 | assert_eq!(description, "fizz"); 193 | assert_eq!(done, false); 194 | assert_eq!(progress, 10.5); 195 | assert_eq!(created_at, NaiveDate::from_ymd(2020, 1, 1).and_hms(1, 1, 1)); 196 | Ok(()) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/mysql/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::loader::Loader; 2 | use crate::mysql::helper; 3 | use chrono::{Offset, TimeZone}; 4 | use sqlx::{MySql, MySqlConnection}; 5 | use std::fmt::Display; 6 | 7 | /// An alias for [Loader](crate::loader::Loader), specialized for **MySQL**. 8 | pub type MySqlLoader = Loader; 9 | 10 | impl MySqlLoader 11 | where 12 | O: Offset + Display + Send + Sync + 'static, 13 | Tz: TimeZone + Send + Sync + 'static, 14 | { 15 | /// Creates a [Loader](crate::loader::Loader), specialized for **MySQL** and Set options. 16 | /// 17 | /// # Example 18 | /// ```rust 19 | /// #[cfg(test)] 20 | /// mod tests { 21 | /// use testfixtures::MySqlLoader; 22 | /// #[async_std::test] 23 | /// async fn test_something() -> anyhow::Result<()> { 24 | /// let loader = MySqlLoader::new(|cfg| { 25 | /// //... 26 | /// }) 27 | /// .await?; 28 | /// Ok(()) 29 | /// } 30 | /// } 31 | /// ``` 32 | pub async fn new(options: F) -> anyhow::Result> 33 | where 34 | F: FnOnce(&mut MySqlLoader), 35 | { 36 | let mut loader = Self::default(); 37 | options(&mut loader); 38 | if loader.location.is_none() { 39 | return Err(anyhow::anyhow!("testfixtures: you need a location")); 40 | } 41 | if loader.pool.is_none() { 42 | return Err(anyhow::anyhow!("testfixtures: you need a pool")); 43 | } 44 | loader.helper = Some(Box::new(helper::MySql::default())); 45 | loader.build_insert_sqls(); 46 | loader 47 | .helper 48 | .as_mut() 49 | .unwrap() 50 | .init(loader.pool.as_ref().unwrap()) 51 | .await?; 52 | Ok(loader) 53 | } 54 | } 55 | 56 | #[cfg(test)] 57 | mod tests { 58 | use crate::mysql::loader::MySqlLoader; 59 | use chrono::Utc; 60 | use sqlx::MySqlPool; 61 | use std::fs::File; 62 | use std::io::Write; 63 | use tempfile::tempdir; 64 | 65 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 66 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 67 | async fn test_new() -> anyhow::Result<()> { 68 | let dir = tempdir()?; 69 | let file_path = dir.path().join("todos.yml"); 70 | let fixture_file_path = file_path.clone(); 71 | let mut file = File::create(file_path)?; 72 | writeln!( 73 | file, 74 | r#" 75 | - id: 1 76 | description: fizz 77 | created_at: 2020/01/01 01:01:01 78 | updated_at: RAW=NOW()"# 79 | )?; 80 | 81 | let pool = MySqlPool::new("fizz").await?; 82 | let loader = MySqlLoader::new(|cfg| { 83 | cfg.location(Utc); 84 | cfg.database(pool); 85 | cfg.skip_test_database_check(); 86 | cfg.files(vec![fixture_file_path.to_str().unwrap()]); 87 | }) 88 | .await?; 89 | 90 | assert_eq!(loader.location.unwrap(), Utc); 91 | assert!(loader.pool.is_some()); 92 | assert!(loader.skip_test_database_check); 93 | assert!(loader.helper.is_some()); 94 | assert_eq!(loader.fixture_files.len(), 1); 95 | Ok(()) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/mysql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod helper; 2 | pub mod loader; 3 | -------------------------------------------------------------------------------- /src/postgresql/helper.rs: -------------------------------------------------------------------------------- 1 | use crate::fixture_file::{FixtureFile, SqlParam}; 2 | use crate::helper::Database as DB; 3 | use async_trait::async_trait; 4 | use chrono::{Offset, TimeZone}; 5 | use sqlx::postgres::PgQueryAs; 6 | use sqlx::{ 7 | arguments::Arguments, postgres::PgArguments, PgConnection, PgPool, Postgres as P, Query, 8 | }; 9 | 10 | /// **PostgreSQL** helper. 11 | pub struct PostgreSql { 12 | pub table_names: Vec, 13 | } 14 | 15 | impl Default for PostgreSql { 16 | fn default() -> Self { 17 | PostgreSql { 18 | table_names: vec![], 19 | } 20 | } 21 | } 22 | 23 | #[async_trait] 24 | impl DB for PostgreSql 25 | where 26 | Tz: TimeZone + Send + Sync + 'static, 27 | O: Offset + Sync + Send + 'static, 28 | { 29 | async fn init(&mut self, _pool: &PgPool) -> anyhow::Result<()> { 30 | Ok(()) 31 | } 32 | 33 | async fn database_name(&self, pool: &PgPool) -> anyhow::Result { 34 | let rec: (String,) = sqlx::query_as("SELECT DATABASE()").fetch_one(pool).await?; 35 | Ok(rec.0) 36 | } 37 | 38 | // TODO: complete this function 39 | // async fn table_names(&self, pool: &PgPool) -> anyhow::Result> { 40 | // let tables = sqlx::query!( 41 | // r#" 42 | // SELECT table_name 43 | // FROM information_schema.tables 44 | // WHERE table_schema = ? AND table_type = 'BASE TABLE'; 45 | // "#, 46 | // "test" 47 | // ) 48 | // .fetch_all(pool) 49 | // .await?; 50 | 51 | // let mut names = vec![]; 52 | // for table in tables { 53 | // names.push(table.table_name) 54 | // } 55 | // Ok(names) 56 | // } 57 | 58 | async fn with_transaction( 59 | &self, 60 | pool: &PgPool, 61 | fixture_files: &[FixtureFile], 62 | ) -> anyhow::Result<()> { 63 | let mut tx = pool.begin().await?; 64 | let result: anyhow::Result<()> = async { 65 | let mut queries = vec![]; 66 | let delete_queries: Vec = fixture_files.iter().map(|x| (x.delete())).collect(); 67 | let mut delete_queries: Vec> = 68 | delete_queries.iter().map(|x| sqlx::query(x)).collect(); 69 | queries.append(&mut delete_queries); 70 | 71 | for fixtures_file in fixture_files { 72 | for sql in &fixtures_file.insert_sqls { 73 | let mut args = PgArguments::default(); 74 | for param in &sql.params { 75 | match param { 76 | SqlParam::String(param) => args.add(param), 77 | SqlParam::Integer(param) => args.add(param), 78 | SqlParam::Datetime(param) => args.add(param.naive_local()), 79 | SqlParam::Float(param) => args.add(param), 80 | SqlParam::Boolean(param) => args.add(param), 81 | } 82 | } 83 | queries.push(sqlx::query(sql.sql.as_str()).bind_all(args)) 84 | } 85 | } 86 | for query in queries { 87 | query.execute(&mut tx).await?; 88 | } 89 | Ok(()) 90 | } 91 | .await; 92 | if result.is_ok() { 93 | tx.commit().await?; 94 | } else { 95 | tx.rollback().await?; 96 | } 97 | Ok(()) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/postgresql/loader.rs: -------------------------------------------------------------------------------- 1 | use crate::loader::Loader; 2 | use crate::postgresql::helper; 3 | use chrono::{Offset, TimeZone}; 4 | use sqlx::{PgConnection, Postgres}; 5 | use std::fmt::Display; 6 | 7 | // TODO: Complete this type. 8 | /// An alias for [Loader](crate::loader::Loader), specialized for **PostgreSQL**. 9 | pub(crate) type PglLoader = Loader; 10 | 11 | impl PostgresLoader 12 | where 13 | O: Offset + Display + Send + Sync + 'static, 14 | Tz: TimeZone + Sync + Send + 'static, 15 | { 16 | pub async fn new(options: F) -> anyhow::Result> 17 | where 18 | F: FnOnce(&mut PostgresLoader), 19 | { 20 | let mut loader = Self::default(); 21 | options(&mut loader); 22 | loader.helper = Some(Box::new(helper::PostgreSql::default())); 23 | loader.build_insert_sqls(); 24 | loader 25 | .helper 26 | .as_mut() 27 | .unwrap() 28 | .init(loader.pool.as_ref().unwrap()) 29 | .await?; 30 | Ok(loader) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/postgresql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod helper; 2 | pub mod loader; 3 | -------------------------------------------------------------------------------- /tests/mysql.rs: -------------------------------------------------------------------------------- 1 | use chrono::{prelude::*, NaiveDate, Utc}; 2 | use sqlx::{cursor::Cursor, mysql::MySqlQueryAs, MySqlPool, Row}; 3 | use std::env; 4 | use std::fs::File; 5 | use std::io::Write; 6 | use std::panic; 7 | use tempfile::tempdir; 8 | use testfixtures::MySqlLoader; 9 | 10 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 11 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 12 | async fn it_returns_ok() -> anyhow::Result<()> { 13 | let dir = tempdir()?; 14 | let file_path = dir.path().join("todos.yml"); 15 | let fixture_file_path = file_path.clone(); 16 | let mut file = File::create(file_path)?; 17 | writeln!( 18 | file, 19 | r#" 20 | - id: 1 21 | description: fizz 22 | done: true 23 | progress: 10.5 24 | created_at: 2020/01/01 01:01:01 25 | - id: 2 26 | description: buzz 27 | done: false 28 | progress: 30.0 29 | created_at: 2020/01/01 02:02:02 30 | - id: 3 31 | description: buzz 32 | done: false 33 | progress: 25.0 34 | created_at: RAW=NOW()"# 35 | ) 36 | .unwrap(); 37 | 38 | let pool = MySqlPool::new(&env::var("TEST_DB_URL")?).await?; 39 | let pool_for_query = pool.clone(); 40 | let loader = MySqlLoader::new(|cfg| { 41 | cfg.location(Utc); 42 | cfg.database(pool); 43 | cfg.paths(vec![fixture_file_path.to_str().unwrap()]); 44 | }) 45 | .await?; 46 | let rec: (i32,) = sqlx::query_as("SELECT count(*) from todos") 47 | .fetch_one(&pool_for_query.clone()) 48 | .await?; 49 | assert!(loader.load().await.is_ok()); 50 | assert_eq!(rec.0, 1); 51 | let mut cursor = sqlx::query("SELECT id, description, done, progress, created_at FROM todos") 52 | .fetch(&pool_for_query); 53 | let row = cursor.next().await?.unwrap(); 54 | let id: u16 = row.get("id"); 55 | let description: String = row.get("description"); 56 | let done: bool = row.get("done"); 57 | let progress: f32 = row.get("progress"); 58 | let created_at: NaiveDateTime = row.get("created_at"); 59 | assert_eq!(id, 1); 60 | assert_eq!(description, "fizz"); 61 | assert_eq!(done, true); 62 | assert_eq!(progress, 10.5); 63 | assert_eq!(created_at, NaiveDate::from_ymd(2020, 1, 1).and_hms(1, 1, 1)); 64 | 65 | let row = cursor.next().await?.unwrap(); 66 | let id: u16 = row.get("id"); 67 | let description: String = row.get("description"); 68 | let done: bool = row.get("done"); 69 | let progress: f32 = row.get("progress"); 70 | let created_at: NaiveDateTime = row.get("created_at"); 71 | assert_eq!(id, 2); 72 | assert_eq!(description, "buzz"); 73 | assert_eq!(done, false); 74 | assert_eq!(progress, 30.0); 75 | assert_eq!(created_at, NaiveDate::from_ymd(2020, 1, 1).and_hms(2, 2, 2)); 76 | 77 | let row = cursor.next().await?.unwrap(); 78 | let id: u16 = row.get("id"); 79 | let description: String = row.get("description"); 80 | let done: bool = row.get("done"); 81 | let progress: f32 = row.get("progress"); 82 | assert_eq!(id, 3); 83 | assert_eq!(description, "buzz"); 84 | assert_eq!(done, false); 85 | assert_eq!(progress, 25.0); 86 | // TODO: check if created_at is the expected value. 87 | Ok(()) 88 | } 89 | 90 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 91 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 92 | async fn it_returns_database_check_error() -> anyhow::Result<()> { 93 | let dir = tempdir()?; 94 | let file_path = dir.path().join("todos.yml"); 95 | let fixture_file_path = file_path.clone(); 96 | let mut file = File::create(file_path)?; 97 | writeln!( 98 | file, 99 | r#" 100 | - id: 1 101 | description: fizz 102 | done: 1 103 | progress: 10.5"# 104 | ) 105 | .unwrap(); 106 | 107 | let pool = MySqlPool::new(&env::var("TEST_DB_URL_FOR_DB_CHECK")?).await?; 108 | let loader = MySqlLoader::new(|cfg| { 109 | cfg.location(Utc); 110 | cfg.database(pool); 111 | cfg.paths(vec![fixture_file_path.to_str().unwrap()]); 112 | }) 113 | .await?; 114 | let result = loader.load().await; 115 | assert!(result.is_err()); 116 | if let Err(err) = result { 117 | assert_eq!( 118 | err.to_string(), 119 | r#"testfixtures: 'fizz' does not appear to be a test database"# 120 | ); 121 | } 122 | Ok(()) 123 | } 124 | 125 | #[cfg_attr(feature = "runtime-async-std", async_std::test)] 126 | #[cfg_attr(feature = "runtime-tokio", tokio::test)] 127 | async fn it_returns_transaction_error() -> anyhow::Result<()> { 128 | let dir = tempdir()?; 129 | let file_path = dir.path().join("todos.yml"); 130 | let fixture_file_path = file_path.clone(); 131 | let mut file = File::create(file_path)?; 132 | writeln!( 133 | file, 134 | r#" 135 | - id: 1 136 | description: fizz 137 | done: 1 138 | progress: 10.5 139 | updated_at: 2020/01/01 01:01:01"# 140 | ) 141 | .unwrap(); 142 | 143 | let pool = MySqlPool::new(&env::var("TEST_DB_URL")?).await?; 144 | let loader = MySqlLoader::new(|cfg| { 145 | cfg.location(Utc); 146 | cfg.database(pool); 147 | cfg.paths(vec![fixture_file_path.to_str().unwrap()]); 148 | }) 149 | .await?; 150 | let result = loader.load().await; 151 | assert!(result.is_err()); 152 | if let Err(err) = result { 153 | assert_eq!( 154 | err.to_string(), 155 | r#"testfixtures: Unknown column 'updated_at' in 'field list'"# 156 | ); 157 | } 158 | Ok(()) 159 | } 160 | --------------------------------------------------------------------------------