Search code examples
scalaunit-testingscalamock

ScalaMock and the Cake Pattern - Why is my stub not called?


I have a Scala app that is using the Cake pattern:

trait RepositoryComponent {
  def repository: Repository

  trait Repository {
    def shouldSave(record: GenericRecord): Boolean
    def findRecord(keys: Array[String]): Long
    def insertRecord(record: GenericRecord)
    def updateRecord(keys: Array[String], record: GenericRecord)
    def cleanUp()
  }
}

trait DbRepositoryComponent extends RepositoryComponent with Logged {
  val connection: Connection
  val subscription: Subscription
  val schema: Schema

  def repository = new DbRepository(connection, subscription, schema)

  class DbRepository(connection: Connection, subscription: Subscription, schema: Schema) extends Repository {
    ...
  }

trait ConsumerComponent {
  def consumer: Consumer

  trait Consumer {
    def write(keys: Array[String], record: GenericRecord)
    def close()
  }
}

trait DbReplicatorComponent extends ConsumerComponent with Logged {
  this: RepositoryComponent =>

  def consumer = new DatabaseReplicator()

  class DatabaseReplicator extends Consumer {
    ...
  }

When I naively started to test the implementation of consumer.write, I tried something like this (using ScalaMock):

class DbReplicatorComponentTests extends FunSuite with MockFactory {

  private val schema = new Schema.Parser().parse(getClass.getResourceAsStream("/AuditRecord.avsc"))
  private val record = new GenericRecordBuilder(schema)
    .set("id1", 123)
    .set("id2", "foo")
    .set("text", "blergh")
    .set("audit_fg", "I")
    .set("audit_ts", 1498770000L)
    .build()

  test("A record should be inserted if the audit flag is 'I' and no existing record is found") {
    val replicator = new DbReplicatorComponent with RepositoryComponent {
      override def repository: Repository = stub[Repository]
    }

    (replicator.repository.shouldSave _).when(record).returns(true)
    (replicator.repository.findRecord _).when(Array("123", "foo")).returns(0L)

    replicator.consumer.write(Array("123", "foo"), record)

    (replicator.repository.insertRecord _).verify(record)
  }
}

The test is failing because I'm not stubbing the actual implementation:

Unsatisfied expectation:

Expected:
inAnyOrder {
  <stub-1> Repository.shouldSave({"id1": 123, "id2": "foo", "text": "blergh", "audit_fg": "I", "audit_ts": 1498770000}) any number of times (never called)
  <stub-2> Repository.findRecord([Ljava.lang.String;@4b8ee4de) any number of times (never called)
  <stub-4> Repository.insertRecord({"id1": 123, "id2": "foo", "text": "blergh", "audit_fg": "I", "audit_ts": 1498770000}) once (never called - UNSATISFIED)
}

Actual:
  <stub-3> Repository.shouldSave({"id1": 123, "id2": "foo", "text": "blergh", "audit_fg": "I", "audit_ts": 1498770000})
ScalaTestFailureLocation: com.generalmills.datalake.sql.DbReplicatorComponentTests at (DbReplicatorComponentTests.scala:12)
org.scalatest.exceptions.TestFailedException: Unsatisfied expectation:

This problem highlights the fact that I really don't grok Scala. I have no idea where stub-3 comes from. I have successfully reworked my tests following damirv's answer in this question, but what I'm hoping for here is some insight to help me better understand why am I not actually mocking what I think I am with the test above?

Fwiw, I come to Scala from a C# background.

UPDATE: based on the answer below, this works:

test("A record should be inserted if the audit flag is 'I' and no existing record is found") {
    val replicator = new DbReplicatorComponent with RepositoryComponent {
      val _repo = stub[Repository]
      override def repository: Repository = {
        _repo
      }
    }

    (replicator.repository.shouldSave _).when(record).returns(true)
    (replicator.repository.findRecord _).when(Array("123", "foo")).returns(0L)

    replicator.consumer.write(Array("123", "foo"), record)

    (replicator.repository.insertRecord _).verify(record)
  }

Solution

  • def repository: Repository this is a method that returns a new stub every time you call it. Try making it a val instead to return the same stub on every invocation/access:

    test("A record should be inserted if the audit flag is 'I' and no existing record is found") {
      val replicator = new DbReplicatorComponent with RepositoryComponent {
        override val repository: Repository = stub[Repository]
      }
      // ...
    }
    

    Also, you are using an Array as a parameter type. Note that Array("foo") != Array("foo") in scala so I suggest you use the argThat matcher in your when call where you use the Array (findRecord) - see here: Unable to create stub with Array argument in ScalMock

    Or try Predicate Matching, as described here: http://scalamock.org/user-guide/matching/