In this article, I am going to show how to use fast-check library to test simple ORM. I am assuming the ORM is ready and will be focusing on the tests. Typescript will be used.

Fast-check

Fast-check is “Property based testing framework for JavaScript (like QuickCheck) written in TypeScript”.

What we are going to test

Mapping

ORM(Object-relational mapping) enables us to work easily with non-scalar objects like Users in a context of a database. First of all, we want to verify that we can the mapping from javascript into SQL primitives and vice versa works.

Queries logic

We also want to verify that logic like filtering, sorting and grouping works according to requirements.

Let’s define our objects

We have Users which we want to store in the ORM.

type Planet = 'Earth' | 'Mars'

type User = { 
    id: string; 
    age: number; 
    email: string; 
    planet: Planet | null
}

Implementation of the ORM is not important. It can be typeorm or any other.

interface UsersOrm {
    add: (user: User) => Promise<void>
    get: (id: string) => Promise<User | null>
    addMany: (users: Array<User>) => Promise<void>
    removeAll: () => Promise<void>
    findUsersFromMars: () => Promise<User[]>
}

In order to use fast-check, we need to define arbitrary for our Users. “Arbitraries — they are responsible for the random but deterministic generation of values, they may also offer shrinking capabilities”.

import fc from 'fast-check'

const userArbitrary: fc.Arbitrary<User> = fc.record({
  id: fc.uuid(),
  age: fc.nat({ max: 200 }),
  email: fc.emailAddress(),
  planet: fc.option(fc.constantFrom('Earth' as const, 'Mars' as const))
})

Now we can generate any number of User objects for our tests!

Testing of the mapping

Now we can verify that mapping from Javascript into SQL and vice versa works.

const orm: UsersOrm = UsersOrmImplementation();

describe('UsersOrm', () => {
  afterEach(() => orm.deleteAll())
  it('should add and get users', () =>
    fc.assert(
      // Fast-check generates some amount(can be controlled globally or per test) 
      // of `User` objects based on the model we had defined in `userArbitrary` 
      fc.asyncProperty(userArbitrary, async (user: User) => {
        // adds user object into database
        await orm.add(user)
        // gets the user back from database 
        const userFromDb = await orm.get(user.id)
        // verify mapping is correct
        expect(userFromDb).toEqual(user)
      })
    ))
})

Testing queries logic

Let’s test that findUsersFromMars returns only users from Mars

it('findUsersFromMars should find users by Mars', () =>
    fc.assert(
      fc.asyncProperty(
        // for any array of users
        fc.array(userArbitrary, { minLength: 3, maxLength: 15 }),
        async (user: User[]) => {
          // adds generated user object into database
          await orm.addMany(user)
          // performs search query
          const searchResultFromDb = await orm.findUsersFromMars()
          // creates expected result
          const expected = user.filter(u => u.planet === 'Mars')
          expect(searchResultFromDb).toEqual(expected)
        }
      )
    ))

Testing of more complex queries

Let’s extend our ORM with selectMany query

enum OrderBy {
  email,
  id,
  planet,
  age
}

interface UsersOrm {
    selectMany: (
        limit: number,
        ofset: number,
        orderBy: OrderBy
    ) => Promise<User[]>
}

Now we can ensure that selectMany works as expected for any arguments

it('selectMany should select users', () =>
    fc.assert(
      fc.asyncProperty(
        // for any array of users
        fc.array(userArbitrary, { minLength: 0, maxLength: 15 }),
        // for any limit, offset, orderBy
        fc.nat({ max: 20 }),
        fc.nat({ max: 20 }),
        fc.constantFrom(...(Object.values(OrderBy) as OrderBy[])),
        // verify that `selectMany` works as expected
        async (users: User[],  
               limit: number, 
               offset: number,
               orderBy: OrderBy
        ) => {
          // adds generated user objects into database
          await orm.addMany(users)
          // performs search query
          const searchResultFromDb = await orm.selectMany(limit, offset, orderBy)
          // creates expected result using javascript
          const expected = performSelection(users, limit, offset, orderBy)
          expect(searchResultFromDb).toEqual(expected)
        }
      )
    ))

Further steps

I demonstrated simple tests for very simple ORM, but this approach can be extended to

  1. more complex queries
  2. more complex objects
  3. stored procedures
  4. schema migrations

Downsides

One of the biggest downsides of this approach is a need to support Arbitrary for each type you store in the ORM. The amount of code can be comparable to the tests themself.

Happy testing!