Factory Time

Posted by Aaron Probus

Factory Time is an open source Clojure library for managing test data.

Intro

When we started building the new investor API we went down the road of keeping things as simple as possible. In order to keep each test as a self contained unit test data was declared at the top of each file. As the project grew larger, this resulted in duplicated test data, and worse, bugs.

Inconsistent type bug we found

; file1_spec.clj
(def my-test-data {:id 1})

; file2_spec.clj
(def my-test-data {:id "1"})

At this point it was time to extract the data out of the individual test files.

The first pass consisted of moving the test data definitions into a separate namespace and removing duplicates. Next, we created a consistent access point to the data by leveraging a multi-method that also handled overriding of values. The result was:

  1. Decomplected test files: Test files became about writing tests, not wading through arbitrary data
  2. Deduplicated test data: All test files had access to the same data, so only one file had to be updated when the data changed
  3. Unified interface for creating test data: No more subtle differences between files (build-data vs build-my-data)

Once the initial pass was completed, it was clear that the data managing functions could exist as a standalone library. From there, we extracted Factory Time into a separate library and added some extra features.

Usage

; people_factory.clj
(deffactory :person {:name "Billy Joe", :age 42})
(deffactory :child {:age 12}
  :extends-factory :person
  :generators {:smart (fn [n] (even? n)} ; n starts at 1 and will increase by 1 every time build is called
  :create! save-child!)

; people_spec.clj
(factory/build :child {:hair-color "red"}) ; {:name "Billy Joe"
                                           ;  :age 12
                                           ;  :smart false
                                           ;  :hair-color "red"}

Calling build performs a multi level merge with the following precendence (lowest to highest):

  1. Parent factory result
  2. Factory defaults
  3. Generated values
  4. Build overrides
  5. create! result (skipped when build is called)

Conclusion

By implementing Factory Time in our project, we saw several key improvements:

  1. Single source for test data: Write your test data once and use it anywhere
  2. More comprehensible tests: No need to skip over 50+ lines of test data to get to the actual tests

If you want to try out Factory Time, take a look at our github page for up to date information.