How do I unit-test saving file to the disk?
I know that it's strongly recommended to run unit-tests in separation from file system, because if you do touch file system in your test, you also test file system itself. OK, that's reasonable.
My question is, if I want to test file saving to the disk, what do I do? As with database, I separate an interface that is responsible for database access, and then create another implementation of this for my tests?开发者_运维百科 Or may be there's some other way?My approach towards this is heavily biased on the Growing Object-Oriented Software Guided by Tests (GOOS) book that I just read, but it's the best that I know of today. Specifically:
- Create an interface to abstract away the file system from your code. Mock it where this class is needed as a collaborator/dependency. This keeps your unit-tests quick and feedback fast.
- Create integration tests that test the actual implementation of the interface. i.e. verify that calling Save() actually persists a file to disk and has the write contents (use a reference file or parse it for a few things that it should contain)
- Create an acceptance test that tests the whole system - end to end. Here you may just verify that a file is created - the intent of this test is to confirm if the real implementation is wired / plugged in correctly.
Update for commenter:
If you're reading structured data (e.g. Book objects) (If not substitute string for IEnumerable)
interface BookRepository
{
IEnumerable<Books> LoadFrom(string filePath);
void SaveTo(string filePath, IEnumerable<Books> books);
}
Now you can use constructor-injection to inject a mock into the client class. The client class unit tests therefore are fast ; do not hit the filesystem. They just verify that the right methods are called on the dependencies (e.g. Load/Save)
var testSubject = new Client(new Mock<BookRepository>.Object);
Next you need to create the real implementation of BookRepository that works off a File (or a Sql DB tommorrow if you want it). No one else has to know. Write integration tests for FileBasedBookRepository (that implements the above Role) and test that calling Load with a reference file gives the right objects and calling Save with a known list, persists them to the disk. i.e. uses real files These tests would be slow so mark them up with a tag or move it to a separate suite.
[TestFixture]
[Category("Integration - Slow")]
public class FileBasedBookRepository
{
[Test]
public void CanLoadBooksFromFileOnDisk() {...}
[Test]
public void CanWriteBooksToFileOnDisk() {...}
}
Finally there should be one/more acceptance tests that exercises Load and Save.
There is a general rule to be cautious of writing unit tests that do file I/O, because they tend to be too slow. But there is no absolute prohibition on file I/O in unit tests.
In your unit tests have a temporary directory set up and torn down, and create test files and directories within that temporary directory. Yes, your tests will be slower than pure CPU tests, but they will still be fast. JUnit even has support code to help with this very scenario: a @Rule
on a TemporaryFolder
.
That said, most file writing code takes this form:
- Open an output stream to the file. This code must cope with missing files or file permissions problems. You will want to test that it opens the file and copes with those failure conditions.
- Write to the output stream. This must write in the correct format, which is the most complicated part requiring the most testing. It must cope with an I/O error while writing to the output stream, although those errors are rare in practice.
- Close the output stream. This must cope with an I/O error while closing the stream, although those errors are rare in practice.
Only the first really deals with the file system. The rest just uses an output stream.
So you could extract the middle and last part to its own method (function), which manipulates a given output stream, rather than a named file. You can then mock that output stream to unit test the method. Those unit tests will be very fast. Most programming languages already provide a suitable output stream class.
That leaves only the first part to be unit tested. You will need only a few tests, so your test suite should still be acceptably fast.
You could instead of passing a filename to your save function, pass a Stream, TextWriter or similar. Then when testing you can pass a memory-based implementation and verify the correct bytes are written without actually writing anything to disk.
To test problems and exceptions you could take a look at a mocking framework. This can help you to artifically generate a specific exception at the certain point in the save process and test that your code handles it appropriately.
精彩评论