How do I mock a Scala Companion Object with EASYMOCK on Scala 3?

212 Views Asked by At

I have the following code...

    class CoreDataSource {
      def getConnection = {
        println("Getting the connection")
        CoreDataSource.getConnection
      }
    }
    object CoreDataSource {
      def getConnection: Option[Connection] = {
        getDataSource.get.getConnection
      }
      def getDataSource: Option[DataSource] = {
        ...
        config = new HikariConfig // This has side effects and can't run
        ...
        val pool : DataSource = new HikariDataSource(config) // This has side effects and can't run
        ...
        Some(pool)
      }
    }

I am trying to mock out the creation of the HikariDataSource and HikariConfig. I tried this...

    class CoreDataSourceSpec extends AnyFunSpec with EasyMockSugar {
      describe("Core Data Source") {
        it("Do something") {
          val cdsMock = mock[CoreDataSource.type]
          ...
        }
      }
    }

But I get

Cannot subclass final class ....CoreDataSource$

What is the proper way to Mock out a companion object using EasyMock

2

There are 2 best solutions below

7
Mateusz Kubuszok On

You don't.

Companion object should only perform pure computations (at least don't contain state) which don't require mocking. Most of the time it's purpose is to store factories and instances of type classes (implicits/givens) for your type.

If you store a mutable state (e.g. connection to the database) in companion you messed up. If you have a Java background, think this way: would you mock static methods of a class? (If you're drifting towards PowerMockito you should reconsider your life choices).

Your example shows that you want to store the connection somewhere - storing it in companion is basically global, shared, mutable state which is universally a bad idea, no matter the background.

Create factory of CoreDataSource in its companion, then pass around CoreDataSource instance directly. No issue with mocking that in your tests.

class CoreDataSource(dataSource: DataSource) {
  def getConnection: Connection =
    dataSource.getConnection
}

object CoreDataSource {

  def createHikari(config: HikariConfig): CoreDataSource =
    new CoreDataSource(new HikariDataSource(config))
}
// in test:

val dataSource = mock[DataSource]
val coreDataSource = new CoreDataSource(dataSource)
// then mock dataSource.getConnection 

Doing it another way requires solving the hard problem that you have 0 reasons to have in the first place. If this companion object is not your but someone else and you cannot rewrite it - wrap it in your own code that you can control and mock easily.


EDIT. In case you are using something like Google Cloud... it still doesn't make sense to store everything in companion and mock it.

// functionality

class MyService(
  connection: Connection
) {

  def someFunctionality(arg: Arg): Result = ...
}
// in test

// given
val connection = mock[Connection] // mocking DB sounds like a bad idea but whatever
val myService = new MyService(connection)

// when
myService.someFunctionality(arg)

// then
// assertions
// adapter for Google Cloud, other cloud solutions should be similar

class MyFunction extends HttpFunction {

   private val config = ...
   private val coreDataSource = CoreDataSource.hikari(config)
   private val connection = coreDataSource.getConnection
   private val myService = new MyService(connection)

  override def service(request: HttpRequest, response: HttpResponse): Unit = {
    // extract data from request, then
    val result = myService.someFunctionality(arg)
    // then send result in response
  }
}

And if you needed to cache these private vals - what you are caching is NOT related to business logic at all, it merely wires things together, like main in Java which is never tested, nor require testing.

So you could implement it like:

class MyFunction extends HttpFunction {

   override def service(request: HttpRequest, response: HttpResponse): Unit = {
    // extract data from request, then
    val result = MyFunction.myService.someFunctionality(arg)
    // then send result in response
  }
}

object MyFunction {

   // dependency injection and initialization
   private val config = ...
   private val coreDataSource = CoreDataSource.hikari(config)
   private val connection = coreDataSource.getConnection
   val myService = new MyService(connection)
}

where wrapper MyFunction is NOT tested, but MyService which does all the job is easily testable.

0
AminMal On

You should definitely read more about the language, beside the fact that the other answer mentioned (which is you should only contain pure class-level functionalities in the companion object), you cannot do it. Why? Because companion objects are singleton objects of a final class, which can access private states of the companion class itself and vice versa (think of it kind of like static data of the class).

The thing is, companion object actually is an object of a final class (which if you want, I can provide more details about them). Final classes cannot be mocked, simply because they are "final", and their behavior cannot be changed (even by its subclasses). And mocking is all about mocking a class behavior, not an object's behavior.