How do I apply TDD to read/write functions?

  softwareengineering

It seems like a chicken and egg problem.

You can make a write function write to some data store, but never know you saved it properly without a tested read function.

You can make a read function read from a data store, but how do you put stuff in that data store, to read, without a tested write function?

EDIT:

I am connecting to and making transactions with an SQL database to save and load objects for use. There’s no point testing the access functions the DB provides, but I wrap such DB functions to serialize/deserialize the objects. I want to be sure I’m writing and reading the right stuff to and from the DB correctly.

It isn’t like add/delete, as @snowman mentions. I want to know that the contents I’ve written are correct, but that requires a well-tested read function. When I read, I want to be sure that my read has correctly created an object equal to what was written; but that requires a well-tested write function.

4

Start with the Read Function.

  • In the test setup: create the database and add test data. either via migration scripts or from a backup. As this is not your code, it doesn’t require a test in TDD

  • In the test: instanciate your repository, point at at your test db and call the Read method. Check that the test data is returned.

Now you have a fully tested read function, you can move to the Write function, which can use the existing Read to verify its own results

8

I often just do a write followed by a read. e.g. (pseudocode)

Foo foo1 = setup some object to write
File tempfile = create a tempfile, possibly in memory 
writeFoo(foo1, tempfile) 
Foo foo2 = readFoo(tempfile) 
assertEquals(foo1, foo2); 
clean-up goes here

Added Later

In addition to this solution being “prgamatic” and “good enough”, one could argue that the other solutions test the wrong thing. Testing whether the strings or SQL statements match is not a terrible idea, I’ve done it myself, but it is testing a side effect, and is fragile. What if you change capitalization, add a field, or update a version number within your data? What if your SQL driver switches the order of calls for efficiency, or your updated XML serializer adds an extra space or changes a schema version?

Now, if you must adhere very strictly to some official spec, then I agree that checking the fine details is appropriate.

9

Don’t. Don’t unit test I/O. It is a waste of time.

Unit test logic. If there’s a lot of logic that you want to test in the I/O code, you should refactor your code to separate the logic of how you do I/O and what I/O you do from the actual business of doing I/O (which is nigh-impossible to test).

To elaborate a bit, if you wanted to test an HTTP server, you should do so through two types of tests: integration tests and unit tests. The unit tests should not interact with I/O at all. That’s slow and introduces lots of error conditions that have nothing to do with the correctness of your code. Unit tests shouldn’t be subject to the state of your network!

Your code should separate:

  • The logic of determining what information to send
  • The logic of determining which bytes to send in order to send a particular bit of information (how do I encode a response etc. into raw bytes), and
  • The mechanism of actually writing those bytes to a socket.

The first two involve logic and decisions and need unit tests.
The last doesn’t involve making many if any decisions and can be tested wonderfully using integration testing.

This is just good design in general, actually, but one of the reasons for that is that it makes it easier to test.


Here are some examples:

  • If you are writing code that gets data from a relational database, you can unit test how you map the data returned from relational queries to your application model.
  • If you are writing code that writes data to a relational database, you can unit test which pieces of data you want to write to the database without actually testing the particular SQL queries you use. For example, you might keep two copies of your application state in memory: a copy representing what the database looks like and the working copy. When you want to sync to the database, you need to diff these and write the differences to the database. You can quite easily unit test that diff code.
  • If you are writing code that reads something from a configuration file, you want to test your configuration file format parser, but with strings from your test source file rather than strings you get from disk.

I do not know if this a standard practice or not but it works fine for me.

In my non-database read write method implementations i use my own type-specific toString() and fromString() methods as implementation details.

These can be easily tested in isolation:

 assertEquals("<xml><car type='porsche'>....", new Car("porsche").toString());

For the actual read write methods i have one integration test that physically read and write in one test

By the way:
Is there something wrong having one test that test read/write together?

2

Known data must be formatted in a known manner. The easiest way to implement this is to use a constant string and compare the result, as @k3b described.

You aren’t limited to constants though. There may be a number of properties of the written data that you can extract using a different kind of parser, such as regular expressions, or even ad hoc probes looking for features of the data.

As for reading or writing the data, it may be useful to have an in-memory file system that allows you to run your tests without the possibility of interference from other parts of the system. If you don’t have access to a good in-memory file system, then use a temporary directory tree.

Use dependency injection and mocking.

You don’t want to test your SQL driver and you don’t want to test if your SQL database is online and set up properly. That would be part of an integration- or system-test. You want to test if your code sends the SQL statements it is supposed to send and if it interprets the responses the way it is supposed to.

So when you have a method/class which is supposed to do something with a database, don’t have it get that database connection by itself. Change it so that the object which represents the database connection is passed to it.

In your production code, pass the actual database object.

In your unit tests, pass a mock object which just behaves like an actual database doesn’t actually contact a database server. Just have it check if it receives the SQL statements it is supposed to receive and then responds with hardcoded responses.

This way you can test your database abstraction layer without even needing an actual database.

5

If you’re using an object relational mapper, there is typically an associated library that can be used to test that your mappings work correctly by creating an aggregate, persisting it, and reloading it from a fresh session, followed by verification of the state against the original object.

NHibernate offers Persistence Specification Testing. It can be configured to work against an in-memory store for fast unit tests.

If you follow the simplest version of the Repository and Unit of Work patterns, and test all of your mappings, you can count on things pretty much working.

LEAVE A COMMENT