Let’s assume I have StartCommandHandler
which has responsibility to create some file with required files. But for doing this I have to give him a set of sub-responsibilities, like:
- Checks whether file exists in the FTP
- If Not downloads files from multiple sources to the temp folder
- Then executes some script in the folder
- Then read generated file after script execution
- Then create zip from that folder
- Then remove that folder
- Then updates database
As a result of that Command Handler, we are creating folder with all required files. And now that folder is ready for another operations.
I have just read "Art of the Unit testing"
. And started to add unit tests. I have followed SOLID
principles as well. Especially, SRP
and DIP
, which are in my opinion prerequisites for Unit Testing.
So, most of that things which I stated above are done with specific interfaces. So, 90% job of that Command Handler is to call methods of dependencies. And 10% is the logic like this:
if(!_dependency1.IsAnySomething()) { _dependency2.Download(); var isScriptNeeded = _dependency2.IsScriptNeeded(); if(isScriptNeeded) { var res = _dependency3.ExecuteScript(); _dependency4.SetScriptResult(res.Info, res.Date, res.State); } _dependency3.Archive(); _dependency5.DeleteTemp(); }
I already tested all dependencies of that command handler. But, hat command handler also includes some small logics like, is download file needed, or temp files are deleted or not and so on…
I have so many question in my mind like:
- May be Unit Testing doesn’t make sense for such units? Integration Test to the rescue? Because, it seems wrong to test whether to check all calls, like whether
DeleteTemp
is called after download, or script is executed or not, or script result is passed in a right way toSetScriptResult
method. Is it GOOD Unit Test? - Is there any way to refactor that class for making it testable?
Advertisement
Answer
Unit tests should test the behavior of your code, and not the implementation of the code.
It is helpful to consider how unit tests add value: they communicate the intended behavior of your code, and they verify that the intended behavior is generated by the implementation. They add value two times in your project lifecycle: first when the code is initially implemented, and second when the code is refactored.
But unit tests can’t add value when refactoring if they are closely tied to a particular implementation.
It’s never a perfect science, but one way to know if you’re testing the behavior or the implementation is to ask “will this unit test break if I refactor?” If refactoring will break the test, then it’s not a good unit test.
It is often not helpful to write a unit test to simply ensures that method A is called, then method B, then method C (or whatever). That’s just going to test that your implementation is your implementation, and it’s likely to hinder rather than help the next developer who wants to refactor the code.
Instead, think about the behaviors and how your code interacts with other objects. Try to tease each of those behaviors into separate objects, and test those objects individually.
For example, you might break the above code up into three different behaviors:
- a cache object that checks if a value doesn’t exist then call a factory to create it,
- a factory object that creates an empty directory, calls a builder object to populate it, and then zips and deletes it
- a builder object that downloads files to a directory and runs scripts it finds there.
Each of those objects has individually testable behavior:
class Cache { Cache(ValueStore store, ValueFactory factory) { ... } object GetValue(object key) { if (!store.HasValue(key)) factory.CreateValue(key); return store.GetValue(key); } } class CacheTest { void GetValue_CallsFactory_WhenValueNotInStore() { // arrange var store = Mock.Of<VaueStore>(_ => _.HasValue() == false); var factory = Mock.Of<ValueFactory>(); var cache = new Cache(store, factory); // act cache.getValue(); // assert Mock.Get(factory).Verify(_ => _.CreateValue(), Times.Once()); } }
You can do a similar breakdown of the factory and builder, and test their behaviors individually.