7 releases (3 stable)

2.0.0 Nov 9, 2020
1.0.1 Jul 11, 2020
1.0.0 Jun 30, 2019
0.1.2 Jun 19, 2019
0.0.1 Dec 9, 2018

#980 in Database interfaces

Download history 46/week @ 2022-12-06 88/week @ 2022-12-13 63/week @ 2022-12-20 20/week @ 2022-12-27 39/week @ 2023-01-03 54/week @ 2023-01-10 32/week @ 2023-01-17 43/week @ 2023-01-24 26/week @ 2023-01-31 43/week @ 2023-02-07 45/week @ 2023-02-14 40/week @ 2023-02-21 29/week @ 2023-02-28 51/week @ 2023-03-07 63/week @ 2023-03-14 75/week @ 2023-03-21

226 downloads per month

MIT license

20KB
132 lines

diesel-factories

An implementation of the test factory pattern made to work with Diesel.

See the documentation for more info.


lib.rs:

This is an implementation the test factory pattern made to work with Diesel.

Example usage:

#[macro_use]
extern crate diesel;

use diesel_factories::{Association, Factory};
use diesel::{pg::PgConnection, prelude::*};

// Tell Diesel what our schema is
// Note unusual primary key name - see options for derive macro.
mod schema {
    table! {
        countries (identity) {
            identity -> Integer,
            name -> Text,
        }
    }

    table! {
        cities (id) {
            id -> Integer,
            name -> Text,
            country_id -> Integer,
        }
    }
}

// Our city model
#[derive(Clone, Queryable)]
struct City {
    pub id: i32,
    pub name: String,
    pub country_id: i32,
}

#[derive(Clone, Factory)]
#[factory(
    // model type our factory inserts
    model = City,
    // table the model belongs to
    table = crate::schema::cities,
    // connection type you use. Defaults to `PgConnection`
    connection = diesel::pg::PgConnection,
    // type of primary key. Defaults to `i32`
    id = i32,
)]
struct CityFactory<'a> {
    pub name: String,
    // A `CityFactory` is associated to either an inserted `&'a Country` or a `CountryFactory`
    // instance.
    pub country: Association<'a, Country, CountryFactory>,
}

// We make new factory instances through the `Default` trait
impl<'a> Default for CityFactory<'a> {
    fn default() -> Self {
        Self {
            name: "Copenhagen".to_string(),
            // `default` will return an `Association` with a `CountryFactory`. No inserts happen
            // here.
            //
            // This is the same as `Association::Factory(CountryFactory::default())`.
            country: Association::default(),
        }
    }
}

// The same setup, but for `Country`
#[derive(Clone, Queryable)]
struct Country {
    pub identity: i32,
    pub name: String,
}

#[derive(Clone, Factory)]
#[factory(
    model = Country,
    table = crate::schema::countries,
    connection = diesel::pg::PgConnection,
    id = i32,
    id_name = identity,
)]
struct CountryFactory {
    pub name: String,
}

impl Default for CountryFactory {
    fn default() -> Self {
        Self {
            name: "Denmark".into(),
        }
    }
}

// Usage
fn basic_usage() {
    let con = establish_connection();

    let city = CityFactory::default().insert(&con);
    assert_eq!("Copenhagen", city.name);

    let country = find_country_by_id(city.country_id, &con);
    assert_eq!("Denmark", country.name);

    assert_eq!(1, count_cities(&con));
    assert_eq!(1, count_countries(&con));
}

fn setting_fields() {
    let con = establish_connection();

    let city = CityFactory::default()
        .name("Amsterdam")
        .country(CountryFactory::default().name("Netherlands"))
        .insert(&con);
    assert_eq!("Amsterdam", city.name);

    let country = find_country_by_id(city.country_id, &con);
    assert_eq!("Netherlands", country.name);

    assert_eq!(1, count_cities(&con));
    assert_eq!(1, count_countries(&con));
}

fn multiple_models_with_same_association() {
    let con = establish_connection();

    let netherlands = CountryFactory::default()
        .name("Netherlands")
        .insert(&con);

    let amsterdam = CityFactory::default()
        .name("Amsterdam")
        .country(&netherlands)
        .insert(&con);

    let hague = CityFactory::default()
        .name("The Hague")
        .country(&netherlands)
        .insert(&con);

    assert_eq!(amsterdam.country_id, hague.country_id);

    assert_eq!(2, count_cities(&con));
    assert_eq!(1, count_countries(&con));
}
#
# fn main() {
#     basic_usage();
#     setting_fields();
#     multiple_models_with_same_association();
# }
# fn establish_connection() -> PgConnection {
#     use std::env;
#     let pg_host = env::var("POSTGRES_HOST").unwrap_or_else(|_| "localhost".to_string());
#     let pg_port = env::var("POSTGRES_PORT").unwrap_or_else(|_| "5432".to_string());
#     let pg_password = env::var("POSTGRES_PASSWORD").ok();
#
#     let auth = if let Some(pg_password) = pg_password {
#         format!("postgres:{}@", pg_password)
#     } else {
#         String::new()
#     };
#
#     let database_url = format!(
#         "postgres://{auth}{host}:{port}/diesel_factories_test",
#         auth = auth,
#         host = pg_host,
#         port = pg_port
#     );
#     let con = PgConnection::establish(&database_url).unwrap();
#     con.begin_test_transaction().unwrap();
#     con
# }

// Utility functions just for demo'ing
fn count_cities(con: &PgConnection) -> i64 {
    use crate::schema::cities;
    use diesel::dsl::count_star;
    cities::table.select(count_star()).first(con).unwrap()
}

fn count_countries(con: &PgConnection) -> i64 {
    use crate::schema::countries;
    use diesel::dsl::count_star;
    countries::table.select(count_star()).first(con).unwrap()
}

fn find_country_by_id(input: i32, con: &PgConnection) -> Country {
    use crate::schema::countries::dsl::*;
    countries
        .filter(identity.eq(&input))
        .first::<Country>(con)
        .unwrap()
}

#[derive(Factory)]

Attributes

These attributes are available on the struct itself inside #[factory(...)].

Name Description Example Default
model Model type your factory inserts City None, required
table Table your model belongs to crate::schema::cities None, required
connection The connection type your app uses MysqlConnection diesel::pg::PgConnection
id The type of your table's primary key i64 i32
id_name The name of your table's primary key column identity id

These attributes are available on association fields inside #[factory(...)].

Name Description Example Default
foreign_key_name Name of the foreign key column on your model country_identity {association_name}_id

Builder methods

Besides implementing Factory for your struct it will also derive builder methods for easily customizing each field. The generated code looks something like this:

struct CountryFactory {
    pub name: String,
}

// This is what gets generated for each field
impl CountryFactory {
    fn name<T: Into<String>>(mut self, new: T) -> Self {
        self.name = new.into();
        self
    }
}
#
# impl Default for CountryFactory {
#     fn default() -> Self {
#         CountryFactory { name: String::new() }
#     }
# }

// So you can do this
CountryFactory::default().name("Amsterdam");

Builder methods for associations

The builder methods generated for Association fields are a bit different. If you have a factory like:

# #![allow(unused_imports)]
# include!("../tests/docs_setup.rs");
#
#[derive(Clone, Factory)]
#[factory(
    model = City,
    table = crate::schema::cities,
)]
struct CityFactory<'a> {
    pub name: String,
    pub country: Association<'a, Country, CountryFactory>,
}
#
# impl<'a> Default for CityFactory<'a> {
#     fn default() -> Self {
#         unimplemented!()
#     }
# }
#
# fn main() {}

You'll be able to call country either with an owned CountryFactory:

# #![allow(unused_imports)]
# include!("../tests/docs_setup.rs");
#
# #[derive(Clone, Factory)]
# #[factory(
#     model = City,
#     table = crate::schema::cities,
# )]
# struct CityFactory<'a> {
#     pub name: String,
#     pub country: Association<'a, Country, CountryFactory>,
# }
#
# impl<'a> Default for CityFactory<'a> {
#     fn default() -> Self {
#         Self {
#             name: String::new(), country: Association::default(),
#         }
#     }
# }
#
# fn main() {
let country_factory = CountryFactory::default();
CityFactory::default().country(country_factory);
# }

Or a borrowed Country:

# #![allow(unused_imports)]
# include!("../tests/docs_setup.rs");
#
# #[derive(Clone, Factory)]
# #[factory(
#     model = City,
#     table = crate::schema::cities,
# )]
# struct CityFactory<'a> {
#     pub name: String,
#     pub country: Association<'a, Country, CountryFactory>,
# }
#
# impl<'a> Default for CityFactory<'a> {
#     fn default() -> Self {
#         Self {
#             name: String::new(), country: Association::default(),
#         }
#     }
# }
#
# fn main() {
let country = Country { id: 1, name: "Denmark".into() };
CityFactory::default().country(&country);
# }

This should prevent bugs where you have multiple factory instances sharing some association that you mutate halfway through a test.

Optional associations

If your model has a nullable association you can do this:

# #![allow(unused_imports)]
# include!("../tests/docs_setup_with_city_factory.rs");
#
#[derive(Clone, Factory)]
#[factory(
    model = User,
    table = crate::schema::users,
)]
struct UserFactory<'a> {
    pub name: String,
    pub country: Option<Association<'a, Country, CountryFactory>>,
#   pub age: i32,
#   pub home_city: Option<Association<'a, City, CityFactory<'a>>>,
#   pub current_city: Option<Association<'a, City, CityFactory<'a>>>,
}

impl<'a> Default for UserFactory<'a> {
    fn default() -> Self {
        Self {
            name: "Bob".into(),
            country: None,
#           age: 30,
#           home_city: None,
#           current_city: None,
        }
    }
}

# fn main() {
// Setting `country` to a `CountryFactory`
let country_factory = CountryFactory::default();
UserFactory::default().country(Some(country_factory));

// Setting `country` to a `Country`
let country = Country { id: 1, name: "Denmark".into() };
UserFactory::default().country(Some(&country));

// Setting `country` to `None`
UserFactory::default().country(Option::<CountryFactory>::None);
UserFactory::default().country(Option::<&Country>::None);
# }

Customizing foreign key names

You can customize the name of the foreign key for your associations like so

# #![allow(unused_imports)]
# include!("../tests/docs_setup.rs");
#
#[derive(Clone, Factory)]
#[factory(
    model = City,
    table = crate::schema::cities,
)]
struct CityFactory<'a> {
    #[factory(foreign_key_name = country_id)]
    pub country: Association<'a, Country, CountryFactory>,
#   pub name: String,
}
#
# impl<'a> Default for CityFactory<'a> {
#     fn default() -> Self {
#         unimplemented!()
#     }
# }
#
# fn main() {}

Dependencies

~3.5MB
~71K SLoC