scala3mock

scala3mock

  • Docs
  • GitHub

›User Guide

Overview

  • Getting Started

User Guide

  • Features
  • Argument matching
  • ScalaTest Integration
  • Cats Integration
  • Advanced Topics
  • FAQ

ScalaTest Integration

Integration

To provide mocking support in ScalaTest, extends your suite with eu.monniot.scala3mock.scalatest.MockFactory:

import org.scalatest.flatspec.AnyFlatSpec
import eu.monniot.scala3mock.scalatest.MockFactory

class CoffeeMachineTest extends AnyFlatSpec with MockFactory {

  "CoffeeMachine" should "not turn on the heater when the water container is empty" in {
      val waterContainerMock = mock[WaterContainer]
      when(() => waterContainerMock.isEmpty).expects().returning(true)
      // ...
  }
}

When using the asynchronous version, please use eu.monniot.scala3mock.scalatest.AsyncMockFactory instead:

import org.scalatest.flatspec.AsyncFlatSpec
import eu.monniot.scala3mock.scalatest.AsyncMockFactory

class ExchangeRateListingTest extends AsyncFlatSpec with AsyncMockFactory {

  val eur = Currency(id = "EUR", valueToUSD = 1.0531, change = -0.0016)
  val gpb = Currency(id = "GPB", valueToUSD = 1.2280, change = -0.0012)
  val aud = Currency(id = "AUD", valueToUSD = 0.7656, change = -0.0024)

  "ExchangeRateListing" should "eventually return the exchange rate between passed Currencies when getExchangeRate is invoked" in {
      val currencyDatabaseMock = mock[CurrencyDatabase]
      
      when(currencyDatabaseMock.getCurrency).expects(eur.id).returns(eur)
      when(currencyDatabaseMock.getCurrency).expects(gpb.id).returns(gpb)
      when(currencyDatabaseMock.getCurrency).expects(aud.id).returns(aud)
      
      val listing = ExchangeRateListing(currencyDatabaseMock)
      
      val future: Future[Double] = listing.getExchangeRate(eur.id, gpb.id)

      future.map { exchangeRate =>
        assert(exchangeRate == eur.valueToUSD / gpb.valueToUSD)
    }
  }
}

Sharing mocks and expectations in ScalaTest

Sometimes multiple test cases will require the same mocks (and more generally the same fixtures like databases, sockets). There are many ways to achieve that goal, Scala3Mock officialy support only two:

  • isolated test cases: clean and simple, recommended when all tests have the same or very similar mocks
  • fixture contexts: more flexible, recommended for complex test suites when a single set of mocks does not fit all test cases

Isolated test cases

If you mix the org.scalatest.OneInstancePerTest trait to your test suite, each test case will be run in its own instance of the class suite. That means each test will get a different copy of the instance variables.

In the suite scope, you can

  • declare instance variables (eg. mocks) that will be used by multiple test cases, and
  • perform common test cases setup (eg. set up various mock expectations).

Because each test case has its own instance, different test cases do not interfere with each other.

import org.scalatest.OneInstancePerTest
import org.scalatest.matchers.should.Matchers

// Please note that this test suite mixes in OneInstancePerTest
class CoffeeMachineTest2 extends AnyFlatSpec with Matchers with OneInstancePerTest with MockFactory {
   // shared objects
   val waterContainerMock = mock[WaterContainer]
   val heaterMock = mock[Heater]
   val coffeeMachine = CoffeeMachine(waterContainerMock, heaterMock)
   
   // you can set common expectations in the suite scope
   when(() => heaterMock.isReady).expects().returning(true)
   
   // and perform common test setup
   coffeeMachine.powerOn()
   
   "CoffeeMachine" should "not turn on the heater when the water container is empty" in {
       coffeeMachine.isOn shouldBe true
       when(() => waterContainerMock.isEmpty).expects().returning(true)
        
       // ...
       coffeeMachine.powerOff()
       coffeeMachine.isOn shouldBe false
   }
   
   it should "not turn on the heater when the water container is overfull" in {
       // each test case uses a separate, fresh test suite so the coffee machine is turned on
       // even if previous test case turned it off
       coffeeMachine.isOn shouldBe true
       // ...
   }
}

Note that if you need to create costly fixtures (eg. establish a database connection), this strategy will duplicate that costly work by the amount of tests. In those cases, we instead recommend to use fixture contexts.

Fixture contexts

Another way of sharing complex context between tests is to have separate fixture context. In practice that means creating a new trait/class that is instantiated per test. Because each test has its own instance, the context isn't shared between tests and because there is only one class suite you can use it to store common and expensive fixtures.

It's really easy to apply to regular test suites:

class CoffeeMachineTest3 extends AnyFlatSpec with Matchers with MockFactory {
   trait Test { // fixture context
       // shared objects
       val waterContainerMock = mock[WaterContainer]
       val heaterMock = mock[Heater]
       val coffeeMachine = CoffeeMachine(waterContainerMock, heaterMock)
   
       // test setup
       coffeeMachine.powerOn()
   }
   
   "CoffeeMachine" should "not turn on the heater when the water container is empty" in new Test {
       coffeeMachine.isOn shouldBe true
       when(() => waterContainerMock.isEmpty).expects().returning(true)
       // ...
   }
   
   // you can extend and combine fixture-contexts
   trait OverfullWaterContainerTest extends Test {
       // you can set expectations and use mocks in the fixture-context
       when(() => waterContainerMock.isOverfull).expects().returning(true)
   
       // and define helper functions
       def sharedComplexLogic(): Unit = {
         coffeeMachine.powerOff()
         // ...
       }
   }
   
   it should "not turn on the heater when the water container is overfull" in new OverfullWaterContainerTest {
       // ...
       sharedComplexLogic()
   }
}

This pattern can also be used with async test suite, but because those needs to return a Future (or equivalent) we cannot just return the fixture context directly.

class ExchangeRateListingTest2 extends AsyncFlatSpec with AsyncMockFactory {

  class Context {
    val eur = Currency(id = "EUR", valueToUSD = 1.0531, change = -0.0016)
    val gpb = Currency(id = "GPB", valueToUSD = 1.2280, change = -0.0012)
    val aud = Currency(id = "AUD", valueToUSD = 0.7656, change = -0.0024)

    val currencyDatabaseMock = mock[CurrencyDatabase]
    
    when(currencyDatabaseMock.getCurrency).expects(eur.id).returns(eur)
    when(currencyDatabaseMock.getCurrency).expects(gpb.id).returns(gpb)
    when(currencyDatabaseMock.getCurrency).expects(aud.id).returns(aud)
    
    val listing = ExchangeRateListing(currencyDatabaseMock)
  }


  "ExchangeRateListing" should "eventually return the exchange rate between passed Currencies when getExchangeRate is invoked" in {
      val ctx = Context()

      val future: Future[Double] = ctx.listing.getExchangeRate(ctx.eur.id, ctx.gpb.id)

      future.map { exchangeRate =>
        assert(exchangeRate == ctx.eur.valueToUSD / ctx.gpb.valueToUSD)
    }
  }
}

Advanced

TODO: how it works under the hood. Give context to debug some weird errors if the MockContext is either not available or corrupted.

← Argument matchingCats Integration →
  • Integration
  • Sharing mocks and expectations in ScalaTest
    • Isolated test cases
    • Fixture contexts
  • Advanced
scala3mock
Docs
Getting StartedFAQ
Copyright (c) 2022-2023 François Monniot