views:

1429

answers:

10

I am writing a component that, given a ZIP file, needs to:

  1. Unzip the file.
  2. Find a specific dll among the unzipped files.
  3. Load that dll through reflection and invoke a method on it.

I'd like to unit test this component.

I'm tempted to write code that deals directly with the file system:

void DoIt()
{
   Zip.Unzip(theZipFile, "C:\\foo\\Unzipped");
   System.IO.File myDll = File.Open("C:\\foo\\Unzipped\\SuperSecret.bar");
   myDll.InvokeSomeSpecialMethod();
}

But folks often say, "Don't write unit tests that rely on the file system, database, network, etc."

If I were to write this in a unit-test friendly way, I suppose it would look like this:

void DoIt(IZipper zipper, IFileSystem fileSystem, IDllRunner runner)
{
   string path = zipper.Unzip(theZipFile);
   IFakeFile file = fileSystem.Open(path);
   runner.Run(file);
}

Yay! Now it's testable; I can feed in test doubles (mocks) to the DoIt method. But at what cost? I've now had to define 3 new interfaces just to make this testable. And what, exactly, am I testing? I'm testing that my DoIt function properly interacts with its dependencies. It doesn't test that the zip file was unzipped properly, etc.

It doesn't feel like I'm testing functionality anymore. It feels like I'm just testing class interactions.

My question is this: what's the proper way to unit test something that is dependent on the file system?

edit I'm using .NET, but the concept could apply Java or native code too.

+3  A: 

One way would be to write the unzip method to take InputStreams. Then the unit test could construct such an InputStream from a byte array using ByteArrayInputStream. The contents of that byte array could be a constant in the unit test code.

nsayer
Ok, so that allows for injection of the stream. Dependency injection/IOC.How about the part of unzipping the stream into files, loading a dll among those files, and calling a method in that dll?
Judah Himango
+2  A: 

Assuming that "file system interactions" are well tested in the framework itself, create your method to work with streams, and test this. Opening a FileStream and passing it to the method can be left out of your tests, as FileStream.Open is well tested by the framework creators.

Sunny
You and nsayer have essentially the same suggestion: make my code work with streams. How about the part about unzipping the stream contents into dll files, opening that dll and calling a function in it? What would you do there?
Judah Himango
+4  A: 

I am reticent to pollute my code with types and concepts that exist only to facilitate unit testing. Sure, if it makes the design cleaner and better then great, but I think that is often not the case.

My take on this is that your unit tests would do as much as they can which may not be 100% coverage. In fact, it may only be 10%. The point is, your unit tests should be fast and have no external dependencies. They might test cases like "this method throws an ArgumentNullException when you pass in null for this parameter".

I would then add integration tests (also automated and probably using the same unit testing framework) that can have external dependencies and test end-to-end scenarios such as these.

When measuring code coverage, I measure both unit and integration tests.

HTH, Kent

Kent Boogaart
Yeah, I hear you. There's this bizarre world you reach where you've decoupled so much, that all your left with is method invocations on abstract objects. Airy fluff. When you reach this point, it doesn't feel like you're really testing anything real. You're just testing interactions between classes.
Judah Himango
+1  A: 

You should not test class interaction and function calling. instead you should consider integration testing. Test the required result and not the file loading operation.

Dror Helper
+3  A: 

There's nothing wrong with hitting the file system, just consider it an integration test rather than a unit test. I'd swap the hard coded path with a relative path and create a TestData subfolder to contain the zips for the unit tests.

If your integration tests take too long to run then separate them out so they aren't running as often as your quick unit tests.

I agree, sometimes I think interaction based testing can cause too much coupling and often ends up not providing enough value. You really want to test unzipping the file here not just verify you are calling the right methods.

JC
How often they run is of little concern; we use a continuous integration server that automatically runs them for us. We don't really care how long they take.If "how long to run" isn't a concern, is there any reason to distiguish between unit and integration tests?
Judah Himango
Not really. But if developers want to quickly run all the unit tests locally its nice to have an easy way to do that.
JC
+1  A: 

This seems to be more of an integration test as you are depending on a specific detail (the file system) that could change, in theory.

I would abstract the code that deals with the OS into it's own module (class, assembly, jar, whatever). In your case you want to load a specific DLL if found, so make an IDllLoader interface and DllLoader class. Have your app acquire the DLL from the DllLoader using the interface and test that .. you're not responsible for the unzip code afterall right?

tap
+7  A: 

There's really nothing wrong with this, it's just a question of whether you call it a unit test or an integration test. You just have to make sure that if you do interact with the file system, there are no unintended side effects. Specifically, make sure that you clean up after youself -- delete any temporary files you created -- and that you don't accidentally overwrite an existing file that happened to have the same filename as a temporary file you were using. Always use relative paths and not absolute paths.

It would also be a good idea to chdir() into a temporary directory before running your test, and chdir() back afterwards.

Adam Rosenfield
+1, however note that `chdir()` is process-wide so you might break the ability to run your tests in parallel, if your test framework or a future version of it supports that.
Graham Lee
A: 

As others have said, the first is fine as an integration test. The second tests only what the function is supposed to actually do, which is all a unit test should do.

As shown, the second example looks a little pointless, but it does give you the opportunity to test how the function responds to errors in any of the steps. You don't have any error checking in the example, but in the real system you may have, and the dependency injection would let you test all the responses to any errors. Then the cost will have been worth it.

David Sykes
A: 

For unit test I would suggest that you include the test file in your project(EAR file or equivalent) then use a relative path in the unit tests i.e. "../testdata/testfile".

As long as your project is correctly exported/imported than your unit test should work.

James Anderson