Handle I/O in TDD based unit tests

Viewed 247

I'm practicing Java.based TDD and write some piece of code, which reads and writes files. Now, I have about 100 tests for each scenario (read and write) to test my code, where I create a file or read a given file everytime. Files to write will be created in temporary directory and will be deleted after every test run. But this strategy produces a lot of I/O and I'm afraid in case of e.g. SSD lifetime. Mocking is not an option.

One possibility would be to reade/write a file once and then run my tests against a (static) data structure (pseudo code):

private static Object resultData = null;

@BeforeClass
// Read/Write my stuff here
resultData = ....

@Test
// Check my requirements
assertTrue(resultData....);

Problem is, that I can change expected behaviour inside test methods, so my tests are not autonomous anymore.

How would you deal with it?

2 Answers

But this strategy produces a lot of I/O and I'm afraid in case of e.g. SSD lifetime.

You overthink about that : the DWPD (Drive Writes Per Day) to 1 for a SSD Disk (which is today standard enough) suits for many use cases.
The DWPD measures how many times you could overwrite the drive’s entire size a day of its life.
For a 500 GO disk, a DWPD to 1 means that theoretically you could write 500 GO per day while the product warranty policy period is not reached.
And that is generally between 5 and 10 years.
So some temporary files created at each build (100 or more) are almost nothing but if you perform some million of build per day.

Besides, you should not make code or test more complex because of this kind of consideration.
SSD is there to make things simpler for developers, not more complex.

That being said, it can still make sense to avoid so much file writing in unit tests because tests has to be fast executed.
You could change the API of the tested class to make it also accept ByteArrayInputStream and ByteArrayOutputStream. In this way reads and writes will work in memory, not with the filesystem.

How would you deal with it?

I would refactor the code so that using test doubles (aka mocking) is an option.

Specifically, I would be looking to create seams such that

  1. The code that implements the I/O is too simple to fail
  2. All of the complicated code doesn't care how the I/O is being implemented.

In a language like java, the seam would be an interface. My I/O code would implement interface, but would not, itself, be run in the test.

Instead, the test would use a suitable test double -- something that implements the same interface, but does not actually perform writes to disk.

Note: there might be several flavors of test double, if I want tests that simulate various sorts of write errors, to ensure that the complicated logic handles those failures properly.

The composition root for the production code would, of course, need to create an instance of the "real" implementation to use. But the composition root for the test can instead select a test double, or an in memory file system, or whatever to ensure that the results of the test are deterministic (and, incidentally, don't chew into your SSD lifetime).

The heuristic for what code lives behind the scene comes to us from Hoare

One way [of constructing a software design] is to make it so simple that there are obviously no deficiencies

Related