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.

This post is written by David Pedersen
David is Backend Engineer at Tonsser
On Twitter On GitHub

Like what you're reading? Then subscribe to our newsletter
No spam, just one email every two weeks with the posts we published in that time