6

I have some code looking like this:

package org.samidarko.actors

import org.samidarko.helpers.Lib

class Monitoring extends Actor {

  override def receive: Receive = {
    case Tick =>
       Lib.sendNotification()
    }
}

Is there a way to mock/stub Lib from ScalaTest like with proxyquire for nodejs?

I read that I could use dependency injection but I would rather not do that

Is my only alternative is to pass my lib as class parameter?

class Monitoring(lib: Lib) extends Actor {

Any advice to make it more testable? Thanks

EDIT:

Xavier Guihot's answer is an interesting approach of the problem but I choose to change the code for testing purpose.

I'm passing the Lib as parameter and I'm mocking with mockito, it makes the code easier to test and to maintain than shadowing the scope.

samidarko
  • 590
  • 7
  • 20

2 Answers2

8

This answer only uses scalatest and doesn't impact the source code:

Basic solution:

Let's say you have this src class (the one you want to test and for which you want to mock the dependency):

package com.my.code

import com.lib.LibHelper

class MyClass() {
  def myFunction(): String = LibHelper.help()
}

and this library dependency (which you want to mock / override when testing MyClass):

package com.lib

object LibHelper {
  def help(): String = "hello world"
}

The idea is to create a class in your test folder which will override/shadow the library. The class will have the same name and the same package as the one you want to mock. In src/test/scala/com/external/lib, you can create LibHelper.scala which contains this code:

package com.lib

object LibHelper {
  def help(): String = "hello world - overriden"
}

And this way you can test your code the usual way:

package com.my.code

import org.scalatest.FunSuite

class MyClassTest extends FunSuite {
  test("my_test") {
    assert(new MyClass().myFunction() === "hello world - overriden")
  }
}

Improved solution which allows setting the behavior of the mock for each test:

Previous code is clear and simple but the mocked behavior of LibHelper is the same for all tests. And one might want to have a method of LibHelper produce different outputs. We can thus consider setting a mutable variable in the LibHelper and updating the variable before each test in order to set the desired behavior of LibHelper. (This only works if LibHelper is an object)

The shadowing LibHelper (the one in src/test/scala/com/external/lib) should be replaced with:

package com.lib

object LibHelper {

  var testName = "test_1"

  def help(): String =
    testName match {
      case "test_1" => "hello world - overriden - test 1"
      case "test_2" => "hello world - overriden - test 2"
    }
}

And the scalatest class should become:

package com.my.code

import com.lib.LibHelper

import org.scalatest.FunSuite

class MyClassTest extends FunSuite {
  test("test_1") {
    LibHelper.testName = "test_1"
    assert(new MyClass().myFunction() === "hello world - overriden - test 1")
  }
  test("test_2") {
    LibHelper.testName = "test_2"
    assert(new MyClass().myFunction() === "hello world - overriden - test 2")
  }
}

Very important precision, since we're using a global variable, it is compulsory to force scalatest to run test in sequence (not in parallel). The associated scalatest option (to be included in build.sbt) is:

parallelExecution in Test := false
Xavier Guihot
  • 54,987
  • 21
  • 291
  • 190
  • Nice. I did try to shadow LibHelper but by creating the Object in the test scope before creating the class instance. Is there a way to stub the results of `LibHelper` to make your solution more flexible? – samidarko Mar 01 '18 at 13:32
  • You showed me how I can shadow the scope by replacing and Object by another object. I wondered if it was possible to stub/mock the methods to make it more dynamic, give different answers from one test to another for example? – samidarko Mar 01 '18 at 14:03
  • @samidarko Updated the answer; hope this helps. – Xavier Guihot Mar 01 '18 at 21:53
  • Interesting solution. Thanks – samidarko Mar 02 '18 at 03:58
  • @XavierGuihot How do you make sure that the test calls the shadowed object as opposed to the original one? wouldn't that depend on the class loader? – Assaf Mendelson Aug 08 '18 at 14:29
  • I think when executing tests, the default behaviour consists in first looking for the class in test sources before looking within project sources and external dependencies. I never witnessed the opposite. – Xavier Guihot Aug 08 '18 at 14:35
2

Not a complete answer (as I don't know AOP very well), but to put you in the right direction, this is possible through Java lib called AspectJ:

https://blog.jayway.com/2007/02/16/static-mock-using-aspectj/

https://www.cakesolutions.net/teamblogs/2013/08/07/aspectj-with-akka-scala

Example in pseudocode (without going into details):

class Mock extends MockAspect {
    @Pointcut("execution (* org.samidarko.helpers.Lib.sendNotification(..))")
    def intercept() {...}

}

The low level basics of this approach are Dynamic Proxies: https://dzone.com/articles/java-dynamic-proxy. However, you can mock static methods too (maybe you'll have to add word static into the pattern).

dk14
  • 22,206
  • 4
  • 51
  • 88