Design by database seed
I like test-driven development. The fact that it's baked right in to Ruby on Rails is one of the things that attracted me to Rails all those years ago.
I find that if I write tests-first, I come up with better, smaller, cleaner code. This is because testing can help to drive the design process - you start from "this is what I need to achieve - given these inputs, I'm expecting these outputs; so how do I want the API to look?" rather than "what classes or tables or services or whatever do I need?".
But another advantage of tests is they serve as living documentation. We've all written some code, put together some docs on how to use it, then when someone joins the organisation a year later, having to apologise and say "it's very out of date but you might find it useful".
Well, tests don't go out of date (at least, not if you make sure they always pass).
Now there's a discussion here about the type of testing I do - it's not unit testing, it's not end-to-end or integration testing. And Rails muddies the water further by using its own terminology. I guess it's feature testing - and it comes from BDD (behaviour driven development).
When someone requests a feature, I write out a little story for how it's going to work.
- Alice does X, which results in A
- Alice takes A and does Y to it, resulting in B
- The system sends an email to Bob.
- Bob sees B and does Z
I write these as "service and model tests" - at the moment I don't do User Interface tests because they take ages to run and often fail randomly (generally timing issues with asynchronous code). But I write them as tests for the various services, models and databases in my system - in effect, testing the business logic, using the API as a starting point.
But writing tests can be a big up-front investment - one that definitely pays off but it does cost.
For my side-project, I had to quickly build a demo system for the original client.
I started off writing the feature tests, as above. But, with time being short, I thought I'm going to have to build a database to demo this to them - so not only am I building a test suite that fills up with data that will be familiar to them, but I'll also have to populate a database with the same.
So I decided to do "design by database seed".
Rails has an inbuilt task - rails db:seed
that loads a seeds.rb
file and uses it to populate the current database. I decided that, to save time, I would write a set of functions that could be called from within my tests and from within the seeds file. I would write the seeds first, do the demo and once I had the go-ahead, write the tests (and yes, I will write the tests because it makes me uncomfortable having untested code).
One thing to note about seeds files - while the documentation has an example that looks something like this:
@thing = Thing.create! name: "whatever"
you should actually make sure your seeds are re-runnable:
@thing = Thing.where(name: "whatever").first_or_create!
That way you can run the seeds again and again as you add new stuff to it.
As it's a blank database, this means I can use the seed file to design how I want the code to work - then go away and implement it. Which is one of the advantages of test-first development. And it serves as a (temporary) document, describing how to use the system.
For example, Standard Procedure has the concept "lists of cards" - and you can perform "actions" on those cards. For this demo, if a card is in the "dispatch required" list then you can invoke the "dispatch" action, providing some notes, and it will then move to the "dispatched" List.
Currently the configuration for this, in the seeds file, looks like:
account.add_action_type(name: "Dispatch", position: 1, user: admin).tap do |a| a.invoked_from dispatch_required_list, user: anna a.add_input name: "Dispatch Notes", field_type: "FieldDefinition::RichText", position: 1, user: anna a.add_destination dispatched_list, user: anna end
The invoked_from
and add_destination
calls went through a few iterations - but my starting point was "how can I get this to do what I want in the simplest, most-readable way possible?" I came back to those calls as I was implementing them, adding or removing parameters and changing the names. invoked_from
originally worked the other way around - dispatch_required_list.add_action_type a
but I wanted it to reflect the words I would say to the client during the demo - "so we're setting up this action and you can use that action from within this list" - not "we're setting up this action and adding it to this list"
And reading this back now, I think that I should remove the named name
parameters and make them the first unnamed parameter in each method call. Design by database seed in action as, now I'm looking at it, I've noticed a way to make the API a tiny bit more readable.
That means it should become:
account.add_action_type "Dispatch", position: 1, user: admin ... a.add_input "Dispatch Notes", field_type: "FieldDefinition::RichText", position: 1, user: anna
(I'll talk about the user
parameters in a later article)
My next step is to build the UI that sits over a populated database, filled with data that the client will recognise.
So this isn't test-driven development. I've done this before and after a while, the seeds file no longer becomes necessary as you start to deal with real problems caused by real data. But as a starting point, when time is short and a demo is looming, it's a great way to design your internal API.