Best practice for Unit Testing class which is mostly responsible to call methods of dependencies, but contains logic as well

Tags: , , , ,



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:

  1. 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 to SetScriptResult method. Is it GOOD Unit Test?
  2. Is there any way to refactor that class for making it testable?

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:

  1. a cache object that checks if a value doesn’t exist then call a factory to create it,
  2. a factory object that creates an empty directory, calls a builder object to populate it, and then zips and deletes it
  3. 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.



Source: stackoverflow