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
226 downloads per month
20KB
132 lines
diesel-factories
An implementation of the test factory pattern made to work with Diesel.
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