Search code examples
scalaunit-testingdependency-injectionspecs2

Testing object which calls another object in Scala using Specs2


I'm working with a project which already has some legacy code written in Scala. I was given a task to write some unit tests for one of its classes when I discovered it's not so easy. Here's the problem I've encountered:

We have an object, say, Worker and another object to access the database, say, DatabaseService which also extends other class (I don't think it matters, but still). Worker, in its turn, is called by higher classes and objects.

So, right now we have something like this:

object Worker {
    def performComplexAlgorithm(id: String) = {
        val entity = DatabaseService.getById(id)
        //Rest of the algorithm
    }
}

My first though was 'Well, I can probably make a trait for DatabaseService with the getById method'. I don't really like the idea to create an interface/trait/whatever just for the sake of testing because I believe it doesn't necessarily lead to a nice design, but let's forget about it for now.

Now, if Worker was a class, I could easily use DI. Say, via constructor like this:

trait DatabaseAbstractService {
    def getById(id: String): SomeEntity
}

object DatabaseService extends SomeOtherClass with DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*complex db query*/}
}

//Probably just create the fake using the mock framework right in unit test
object FakeDbService extends DatabaseAbstractService {
    override def getById(id: String): SomeEntity = {/*just return something*/}
}

class Worker(val service: DatabaseService) {

    def performComplexAlgorithm(id: String) = {
        val entity = service.getById(id)
        //Rest of the algorithm
    }
}

The problem is, Worker is not a class so I can't make an instance of it with another service. I could do something like

object Worker {
    var service: DatabaseAbstractService = /*default*/
    def setService(s: DatabaseAbstractService) = service = s
}

However, it scarcely makes any sense to me since it looks awful and leads to an object with mutable state which doesn't seem very nice.

The question is, how can I make the existing code easily testable without breaking anything and without making any terrible workarounds? Is it possible or should I change the existing code instead so that I could test it easier?

I was thinking about using extending like this:

class AbstractWorker(val service: DatabaseAbstractService)

object Worker extends AbstractWorker(DatabaseService)

and then I somehow could create a mock of Worker but with different service. However, I didn't figure out how to do it.

I'd appreciate any advice as to how either change the current code to make it more testable or test the existing.


Solution

  • If you can alter the code for Worker, you can change it to still allow it to be an object and also allow for swapping of the db service via an implicit with a default definition. This is one solution and I don't even know if this is possible for you, but here it is:

    case class MyObj(id:Long)
    
    trait DatabaseService{
      def getById(id:Long):Option[MyObj] = {
        //some impl here...
      }
    }
    
    object DatabaseService extends DatabaseService
    
    
    object Worker{
      def doSomething(id:Long)(implicit dbService:DatabaseService = DatabaseService):Option[MyObj] = {
        dbService.getById(id)
      }
    }
    

    So we set up a trait with concrete impl of the getById method. Then we add an object impl of that trait as a singleton instance to use in the code. This is a good pattern to allow for mocking of what was previously only defined as an object. Then, we make Worker accept an implicit DatabaseService (the trait) on it's method and give it a default value of the object DatabaseService so that regular use does not have to worry about satisfying that requirement. Then we can test it like so:

    class WorkerUnitSpec extends Specification with Mockito{
    
      trait scoping extends Scope{
        implicit val mockDb = mock[DatabaseService]
      }
    
      "Calling doSomething on Worker" should{
        "pass the call along to the implicit dbService and return rhe result" in new scoping{
          mockDb.getById(123L) returns Some(MyObj(123))
          Worker.doSomething(123) must beSome(MyObj(123))
        }
      }
    

    Here, in my scope, I make an implicit mocked DatabaseService available that will supplant the default DatabaseService on the doSomething method for my testing purposes. Once you do that, you can start mocking out and testing.

    Update

    If you don't want to take the implicit approach, you could redefine Worker like so:

    abstract class Worker(dbService:DatabaseService){
      def doSomething(id:Long):Option[MyObj] = {
        dbService.getById(id)
      }   
    }
    
    object Worker extends Worker(DatabaseService)
    

    And then test it like so:

    class WorkerUnitSpec extends Specification with Mockito{
    
      trait scoping extends Scope{
        val mockDb = mock[DatabaseService]
        val testWorker = new Worker(mockDb){}
      }
    
      "Calling doSomething on Worker" should{
        "pass the call along to the implicit dbService and return rhe result" in new scoping{
          mockDb.getById(123L) returns Some(MyObj(123))
          testWorker.doSomething(123) must beSome(MyObj(123))
        }
      }
    }
    

    In this way, you define all the logic of importance in the abstract Worker class and that's what you till focus your testing on. You provide a singleton Worker via an object that is used in the code for convenience. Having an abstract class let's you use a constructor param to specify the database service impl to use. This is semantically the same as the previous solution but it's cleaner in that you don't need the implicit on every method.