-1

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

Jackie
  • 21,969
  • 32
  • 147
  • 289

2 Answers2

3

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.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64
  • It needs to be globally shared that is the point of a connection pool AFAIK. If I don't keep it globally shared it resets every time the cloud function resets – Jackie Jul 26 '22 at 12:53
  • I am also a bit confused as to how the companion object is using the Hikari config here because it seems I would need to create it before passing it. Would I create an apply here to handle the default case then use the factory? I need the datasource to be created when the function is created and kept globally static. I only need to replace it out for the testing. – Jackie Jul 26 '22 at 13:00
  • If your "cloud function" is destroyed then why you believe that the rest of JVM is kept in memory? – Mateusz Kubuszok Jul 26 '22 at 18:57
  • And if the function merely ends running but is stores in memory as object (e.g as implementation of `HttpFunction` in Google Cloud), why not using its constructor to intialize all dependencies with good old dependency injection? – Mateusz Kubuszok Jul 26 '22 at 19:26
  • Not sure I am quite following can you find an example of doing something similar? I will look and see what I can find. – Jackie Jul 26 '22 at 19:55
  • My assumption was the HttpFunction itself was done in a runnable and the other JVM was around regardless of the number of runnables (function instances) created and I didn't want each one to have its own pool I wanted the pool shared across all instances. – Jackie Jul 26 '22 at 19:59
  • Whatever your assumption is, you can write a normal dependency injection code, and THEN cache and store globally the outcome, if there is no other way. Rather than having each piece stored in a global, and then compose them later and lament over non-testability of globals. You cannot both store things directly in static and ask them to be "mockable". – Mateusz Kubuszok Jul 26 '22 at 22:47
1

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.

AminMal
  • 3,070
  • 2
  • 6
  • 15