Fixtures support for simplified testing of complex model structures

Having started my first proper Vapor 4 project with just 3 model types which have relationships with each other, I already have to write a lot of boilerplate code to setup the data needed for testing specific situations. While I'm starting to refactor that code into an example data generation system that works for my needs, I wonder if it wouldn't make sense to have a flexible and native fixtures support in Vapor.

I'm talking about something like the native Fixtures support in Ruby on Rails or the very popular FactoryBot library which is designed as an alternative, but serves the same purpose. You basically define one or multiple data sets per model type with sample data (a Faker library can further simplify the process) and can quickly create instances of a type, including required relationships if needed in your tests. It can reduce code duplication quite a bit and thus helps following the DRY principle.

I think the first step here would be to create a library. Would anyone be interested in creating such a thing? Or is there already something that I have missed which I could use and help improving?

There are a number of reasons why something like this doesn't exist yet. Swift's reflection capabilities are currently pretty more, so getting access to types and properties isn't easy. Additionally, because Swift is statically typed, the compiler needs to know everything up front. This is very different to Objective-C where you could swizzle your way around everything. It's the main reason why libraries like OCMock will never fully work with Swift. Additionally, Vapor is very protocol and generics based, which necessitates the use of either Structs or final class, which removes the ability to subclass.

Having worked on large projects with lots of tests, I've never really found this a problem TBH. Making use of helper functions to generate test data removes a lot of the pain and I've never really felt the need for anything else. My 2C

Great, so you are using some helpers to fix this problem, too. What I'm concerned about is the less experienced developers who want to learn backend development in Swift, I think we should provide more guidance for them so it's easier for them to get into backend development. And especially since tests are a way too often neglected topic in iOS app projects (at least I've seen too many without any tests written at all), I think we should take this seriously as otherwise chances are high those developers will get frustrated with writing tests and neglect proper tests on backen projects, too. Tests are very important on backends though and as mentioned in this thread, I think Vapor is lacking proper documentation at the moment.

But we can change that easily, as you've stated that you have a helper, I'd like to share my helper structure for fixtures that I have currently in place, if you then also share yours we can maybe learn from each other and in the end write a documentation page which mentions an example structure.

I've setup a protocol called Fixture in my AppTests folder (in the Supporting subfolder):

import Foundation
import Fluent

protocol Fixture: AnyObject {
    associatedtype VariantType

    static func fixture(on database: Database, variant: VariantType, overrides: ((Self) -> Void)?) throws -> Self
}

extension Fixture {
    @discardableResult
    static func fixtures(
        on database: Database,
        variant: VariantType,
        count: Int,
        enumeratedOverrides: ((Int) -> ((Self) -> Void))? = nil
    ) throws -> [Self] {
        try (1 ... count).map { try fixture(on: database, variant: variant, overrides: enumeratedOverrides?($0)) }
    }
}

Then, for each model type, I add an extension in the AppTests folder (within the Fixtures subfolder), here's a simple one for the User model:

@testable import App
import Fluent
import Foundation
import HandySwift

extension User: Fixture {
    enum Variant {
        case minimal
    }

    static func fixture(on database: Database, variant: Variant, overrides: ((User) -> Void)? = nil) throws -> User {
        let instance = User()

        switch variant {
        case .minimal:
            instance.countryCode = ["DE", "US", "JP"].randomElement()!
            instance.subTerritoryCode = ["BW", "CA", "13"].randomElement()!
            instance.timeZoneSecondsFromGMT = Int(TimeInterval.hours([2, -9, 8].randomElement()!).seconds)
        }

        overrides?(instance)

        try instance.save(on: database).wait()
        return instance
    }
}

(Yes, my User type doesn't contain any personal information as name, age etc. which are typically used in User examples. I'm working on a privacy respecting app right now which doesn't save any data I don't need, so sorry for the untypical example.)

Note that I'm specifying arrays at the moment and use randomElement()! to get sample data. I'm planning to replace this part with a faker library later on, but I haven't found one that I like yet.

The basic idea here is that for each different sample data sets you create a new case in the Variant enum and provide the sample data for it in the fixture function. There's also a function as an extension to Fixture which supports creating an array of model objects at once.

Usage in tests is like this:

// create a single user and reference it locally
let user = try User.fixture(on: app.db, variant: .minimal)
// creating multiple users and reference them locally
let users = try User.fixtures(on: app.db, variant: .minimal, count: 5)

You can also use the overrides or enumeratedOverrides closure to do some custom changes to the generated model object on the usage side.

@0xTim What does your solution look like, can you extract an example so we can all learn? :)

Terms of Service

Privacy Policy

Cookie Policy