Creating test data in Rust
If you’re familiar with automated testing in Ruby on Rails you’ve probably heard about factory_bot (formerly known as factory_girl). It is a library for defining factories that can be used to create data during tests.
Using factory_bot in Ruby, defining a factory can be done like so:
factory :country do
name "Denmark"
code "DK"
end
factory :city do
name "Copenhagen"
country
end
In tests you can then use your Ruby factories like so:
city = create :city
country = create :country, city: city
expect(country.city).to eq city
The majority of the tests in our Ruby on Rails API’s test suite uses factories so now that we’re looking to transition part of our backend setup to Rust we needed a similar library there. We did some digging but found no libraries that integrated with Diesel. So naturally we decided to create our own! Version 0.1 of that was released a couple of weeks ago and we’re already using it quite a bit internally. In this blog post I would like to introduce it and show how we’re using it in our tests.
The crate is called “diesel-factories” and you can find it on crates.io here.
Defining factories
Factories in diesel-factories are just normal structs, which implement the Factory
trait. You’re free to implement this trait manually, but the library also provides a “derive macro” which we recommend you use.
Here are the two Ruby factories shown above converted to Rust:
#[derive(Clone, Factory)]
#[factory(model = "Country", table = "countries")]
struct CountryFactory {
pub name: String,
pub code: String,
}
#[derive(Clone, Factory)]
#[factory(model = "City", table = "cities")]
struct CityFactory<'a> {
pub name: String,
pub country: Association<'a, Country, CountryFactory>,
}
Association<'a, Country, CountryFactory>
means that a CityFactory
has an association to a either a Country
or a CountryFactory
. More on that later.
For setting the default values for each field we use the Default
trait from the standard library:
impl Default for CountryFactory {
fn default() -> Self {
Self {
name: "Denmark".to_string(),
code: "DK".to_string(),
}
}
}
impl<'a> Default for CityFactory<'a> {
fn default() -> Self {
Self {
name: "Copenhagen".to_string(),
country: Association::default(),
}
}
}
It is totally possible to derive these implementations with another “derive macro” but that isn’t something we have explored yet. If you think it’d be a good feature, we always welcome pull requests.
Using factories
Using the factories could looks like this:
let country = CountryFactory::default().insert(&db);
let city = CityFactory::default().country(&country).insert(&db);
assert_eq!(country.city_id, city.id);
Where db
is a Diesel database connection, PgConnection
in our case.
Lets unpack what is going on here.
let country = CountryFactory::default().insert(&db);
This line creates a new CountryFactory
instance using the Default
implementation we wrote. .insert(&db)
will then insert that country into the database. This method is defined on the Factory
trait so the code for that is generated by #[derive(Factory)]
.
The next line looks similar:
let city = CityFactory::default().country(&country).insert(&db);
However since we want the city to be associated with the country we just created we call .country(&country)
. This will set the city’s country to &country
. If we didn’t do this the city would get its own new country implicitly. This new country gets set on this line in CityFactory
’s implementation of Default
:
country: Association::default()
In this case Association::default()
is a shorthand for Association::Factory(CountryFactory::default())
.
Factories automatically track if their associations have been inserted or not. So calling .insert
on a CityFactory
that has a CountryFactory
that hasn’t been inserted yet will automatically insert the country as well.
You can also override other default values by calling builder style methods:
CountryFactory::default()
.name("Germany")
.code("DE")
.insert(&db)
These methods are also generated by #[derive(Factory)]
.
Defining more defaults
One of the benefits of factories being regular structs is that we can add methods to them. For example at Tonsser we often name test users Bob, Alice, or Cindy. We can easily wrap that up in a couple of methods like so:
impl UserFactory<'_> {
pub fn bob() -> Self {
Self::default()
.slug("bob-bobsen")
.first_name("Bob")
.last_name("Bobsen")
}
pub fn alice() -> Self {
Self::default()
.slug("alice-alicesen")
.first_name("Alice")
.last_name("Alicesen")
}
pub fn cindy() -> Self {
Self::default()
.slug("cindy-cindysen")
.first_name("Cindy")
.last_name("Cindysen")
}
}
We can then call those methods like so:
let bob = UserFactory::bob().insert(&db);
let alice = UserFactory::alice().insert(&db);
let cindy = UserFactory::cindy().insert(&db);
This feature is called “traits” in factory_bot but we get it for free since we don’t use a DSL for defining factories.
Conclusion
In our Rust test suite we already have 14 of these factories and so far we’re very happy with how they work. I invite you checkout the documentation for diesel-factories if you want to know more.
At Tonsser we believe strongly in the value of open source and have been working on various GraphQL related libraries of the last couple of months. Namely juniper-from-schema and juniper-eager-loading. If you’re interested in Rust and GraphQL I highly encourage you to check them out. You’re also welcome to reach out to me on Twitter if you want to know more.