Test-Driven Database Design

One of the main advantages of DataConstructor is that it lets you drive your database from tests. The feature that makes TDD database development so effective with DataConstructor is versioning. The ability to upgrade a database to an arbitrary version gives you a lot of visibility into the upgrade process itself.

Click here to get started on your free 30-day trial.

We have found that it is best to write three kinds of tests:

  • Acceptance tests
  • Unit tests
  • Transition tests

You don't have to write a new test in each category for each version.  Sometimes you will leave the acceptance tests alone and focus on transition and unit tests.  Sometimes you will be writing new acceptance tests and not feel the need to test a transition.

Acceptance Tests

Acceptance tests prove what your database does.  It's no different from traditional software development.  In fact, most users don't write acceptance tests specifically for the database.  Instead, they integrate DataConstructor into their FIT fixtures.  Doing so will give you certainty: you'll know that the upgrade path applied to production has been tested to death by your automated builds.  Because requirements change, acceptance tests can change.

Acceptance tests that touch the database should always target the "current" version of the database.  It's up to you to define what "current" means for your team but a good rule of thumb would be "whatever version you're going to release, next."  If you integrate DataConstructor  directly into your FIT fixtures, then you'll have a really good idea of which version is "current:" it's the version that makes all of your tests pass.  Testing the "current" version of your database gives you certainty in what your database does, now.

If you write acceptance tests just for your database we've found it's best to route them through at least one layer of indirection.  For instance, acceptance testing your database through a data object persistence layer is probably going to work better than doing so with direct SQL queries.

Good acceptance tests should prove that your software is doing its job.  For instance "I can't add an Order Line Item without an Inventory Item Type," is an acceptance test.  By contrast, a test proving the existence of a foreign key relationship between OrderLineItems and InventoryItemTypes would be more along the lines of a unit test.

Unit Tests

Unit tests prove how your database does what it does.  If you're using proper encapsulation there will be certain aspects of a well-designed database that cannot be tested by a good acceptance test.  In addition: If your acceptance tests target your entire system, then it's going to be really difficult to cover every aspect of your database design without the use of unit tests.  Unit tests should come and go as requirements change but they should be cohesive enough that they don't really need to change.

Unit tests should target a group of versions.  How you define the grouping is up to you.  A good example would be "This unit test applies to the latest 1.3.x versions of the database."  If you define your unit test using that rule, it will "follow" you for a little while and then, when you move to version 1.4 of your database, it will "settle in" on the last version before 1.4.  Leaving a unit test intact, and testing an older version of your database gives you certainty in the history of your database.  This is a key element of growing your database: if you don't know what your database was you probably don't really know what it is.

Unit tests should prove that you've not only done your job but that you've done it right.  As previously mentioned: certain aspects of your database might be difficult to test otherwise.  For instance, your business services layer probably already enforces relationships.  So, technically, relational integrity is not required at the database level, right?  What if someone, down the line, decides to write an application that goes directly to the database (which they shouldn't be doing but... you know).  Each part of a system must be whole and correct in its own right.  The server should never trust the client and the database should never trust the server.

Unit tests give you detailed visibility into the nature of your database and let you develop certain aspects - such as foreign keys - from a test-driven perspective.  That's great for testing what you have now or what you had in the past, and so are acceptance tests, but how do we test that our database changes in a controlled, predictable, and safe way?  That's where transition tests come in to play.

Transition Tests

Transition tests are not required for code.  The transition usually goes like this: "uninstall the old, install the new."  We can't get away with that in the database world.  When you roll out a change, you are, inherently, exposing ostensibly valuable data to risk.  If something goes wrong, the very best you can hope for is that a backup will be restored and your sprint is considered a failure.  Transition tests are all about mitigating this risk.

A transition test goes more like: "Assume I have five rows in the OrderLineItems table with two distinct strings in the ItemType text column.  After the transition, I want to have five OrderLineItems related to two OrderLineItemTypes rows."  They always start by putting the database in one version and populating it with some data.  Then they transition to the very next version and prove that some change went smoothly.

Transition tests, in conjunction with acceptance and unit tests, complete the database testing picture.  They give you certainty in where you are going and certainty in how you got where you are.  In addition, they are the kind of test that most often force you to respect production data.  Proper use of all three kinds of tests can give you the same kind of confidence in your database that you have in your business services layer.  DataConstructor  is designed to facilitate them all.