scala3mock

scala3mock

  • Docs
  • GitHub

›User Guide

Overview

  • Getting Started

User Guide

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

Advanced Topics

First let's import the mocking functions for all our topics

import eu.monniot.scala3mock.ScalaMocks.*

Mocking overloaded, curried and polymorphic methods

Overloaded, curried and polymorphic methods can be mocked by specifying either argument types or type parameters.

Overloaded methods

trait Foo {
  def overloaded(x: Int): String
  def overloaded(x: String): String
  def overloaded[T](x: T): String
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$1@4248959d

when(fooMock.overloaded(_: Int)).expects(10)
// res0: CallHandler1[Int, String] = <advanced-topics.md#L31> Foo.overloaded(10) once (never called - UNSATISFIED)
when(fooMock.overloaded(_: String)).expects("foo")
// res1: CallHandler1[String, String] = <advanced-topics.md#L31> Foo.overloaded(foo) once (never called - UNSATISFIED)
when(fooMock.overloaded[Double]).expects(1.23)
// res2: CallHandler1[Double, String] = <advanced-topics.md#L31> Foo.overloaded[T](1.23) once (never called - UNSATISFIED)

Polymorphic methods

trait Foo {
  def polymorphic[T](x: List[T]): String
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$2@2fe36168

when(fooMock.polymorphic(_: List[Int])).expects(List(1, 2, 3))
// res3: CallHandler1[List[Int], String] = <advanced-topics.md#L58> Foo.polymorphic[T](List(1, 2, 3)) once (never called - UNSATISFIED)

Curried methods

trait Foo {
  def curried(x: Int)(y: Double): String
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$3@2f042123

when(fooMock.curried(_: Int)(_: Double)).expects(10, 1.23)
// res4: CallHandler2[Int, Double, String] = <advanced-topics.md#L75> Foo.curried(10, 1.23) once (never called - UNSATISFIED)

The project have an action item to make the following statement compile:

trait Foo {
  def curried(x: Int)(y: Double): String
}

val fooMock = mock[Foo]

when(fooMock.curried)
// java.lang.ClassCastException: class eu.monniot.scala3mock.functions.MockFunction2 cannot be cast to class eu.monniot.scala3mock.functions.MockFunction1 (eu.monniot.scala3mock.functions.MockFunction2 and eu.monniot.scala3mock.functions.MockFunction1 are in unnamed module of loader sbt.internal.LayeredClassLoader @104d5261)
//  at repl.MdocSession$MdocApp.$init$$$anonfun$1(advanced-topics.md:93)

At the moment the when macro isn't smart enough to recognize a curried function whereas the mock macro is. That result in the underlying mock correctly creating a MockFunction2[Int, Double, String] (two args) but the when macro exposes a MockFunction1[Int, Double => String] (one arg).

Methods with implicit parameters

This case is very similar to curried methods. All you need to do is to help the Scala compiler know that memcachedMock.get should be converted to MockFunction2. For example:

class Codec()

trait Memcached {
  def get(key: String)(implicit codec: Codec): Option[Int]
}

val memcachedMock = mock[Memcached]
// memcachedMock: Memcached = repl.MdocSession$MdocApp$MemcachedMock$1@79b5caf9

implicit val codec = new Codec
// codec: Codec = repl.MdocSession$MdocApp$Codec$1@3ac947b3
when(memcachedMock.get(_ : String)(_ : Codec)).expects("some_key", *).returning(Some(123))
// res5: CallHandler2[String, Codec, Option[Int]] = <advanced-topics.md#L109> Memcached.get(some_key, *) once (never called - UNSATISFIED)

Repeated parameters

Repeated parameters are represented as a Seq. For example, given:

trait Foo {
  def takesRepeatedParameter(x: Int, ys: String*): Unit
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$5@3ed6b7ae

when(fooMock.takesRepeatedParameter).expects(42, Seq("red", "green", "blue"))
// res6: CallHandler2[Int, Seq[String], Unit] = <advanced-topics.md#L130> Foo.takesRepeatedParameter(42, List(red, green, blue)) once (never called - UNSATISFIED)

Returning values (onCall)

By default mocks and stubs return null. You can return predefined value using the returning method. When the returned value depends on function arguments, you can return the computed value (or throw a computed exception) with onCall. For example:

trait Foo {
    def increment(a: Int): Int
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$6@4c0fce1a

when(fooMock.increment).expects(12).returning(13)
// res7: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(12) once (called once)
assert(fooMock.increment(12) == 13)

when(fooMock.increment).expects(*).onCall { (arg: Int) => arg + 1}
// res9: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(*) once (called once)
assert(fooMock.increment(100) == 101)

when(fooMock.increment).expects(*).onCall { arg => throw new RuntimeException("message") }
// res11: CallHandler1[Int, Int] = <advanced-topics.md#L147> Foo.increment(*) once (called once)
try {
    fooMock.increment(0)
    false
} catch {
    case _: RuntimeException => true
    case _ => false
}
// res12: Boolean = true
val mockIncrement = mockFunction[Int, Int]
// mockIncrement: MockFunction1[Int, Int] = MockFunction1
mockIncrement.expects(*).onCall { (arg: Int) => arg + 1 }
// res13: CallHandler1[Int, Int] = MockFunction1(*) once (called once)
assert(mockIncrement(10) == 11)

Call count

By default, mocks expect exactly one call. Alternative constraints can be set with repeated:

val mockedFunction = mockFunction[Int, Int]
// mockedFunction: MockFunction1[Int, Int] = MockFunction1

mockedFunction.expects(42).returns(42).repeated(3 to 7)
// res15: CallHandler1[Int, Int] = MockFunction1(42) between 3 and 7 times (never called - UNSATISFIED)
mockedFunction.expects(3).repeated(10)
// res16: CallHandler1[Int, Int] = MockFunction1(3) between 10 and 2147483646 times (never called - UNSATISFIED)

There are various aliases for common expectations:

val mockedFunction1 = mockFunction[Int, String]
// mockedFunction1: MockFunction1[Int, String] = MockFunction1
val mockedFunction2 = mockFunction[Int, String]
// mockedFunction2: MockFunction1[Int, String] = MockFunction1
val mockedFunction3 = mockFunction[Int, String]
// mockedFunction3: MockFunction1[Int, String] = MockFunction1
val mockedFunction4 = mockFunction[Int, String]
// mockedFunction4: MockFunction1[Int, String] = MockFunction1
val mockedFunction5 = mockFunction[Int, String]
// mockedFunction5: MockFunction1[Int, String] = MockFunction1

mockedFunction1.expects(1).returning("foo").once
// res17: CallHandler1[Int, String] = MockFunction1(1) once (called once)
mockedFunction2.expects(2).returning("foo").noMoreThanTwice
// res18: CallHandler1[Int, String] = MockFunction1(2) no more than twice (never called)
mockedFunction3.expects(3).returning("foo").repeated(3)
// res19: CallHandler1[Int, String] = MockFunction1(3) between 3 and 2147483646 times (called 3 times)
mockedFunction4.expects(4).returning("foo").repeated(atLeast = 1, atMost = 2)
// res20: CallHandler1[Int, String] = MockFunction1(4) between 1 and 2 times (called twice)
mockedFunction5.expects(5).returning("foo").exactly(2)
// res21: CallHandler1[Int, String] = MockFunction1(5) twice (called twice)
mockedFunction5.expects(6).returning("foo").never
// res22: CallHandler1[Int, String] = MockFunction1(6) never (never called)

mockedFunction1(1)
// res23: String = "foo"

mockedFunction3(3)
// res24: String = "foo"
mockedFunction3(3)
// res25: String = "foo"
mockedFunction3(3)
// res26: String = "foo"

mockedFunction4(4)
// res27: String = "foo"
mockedFunction4(4)
// res28: String = "foo"

mockedFunction5(5)
// res29: String = "foo"
mockedFunction5(5)
// res30: String = "foo"

For a complete list, see handlers.CallHandler.

Exceptions

Instead of returning a value, mock can be instructed to throw an exception. This can be achieved either by throwing an exception in onCall or by using the throws method.

trait Foo {
  def increment(a: Int): Int
}

val fooMock = mock[Foo]
// fooMock: Foo = repl.MdocSession$MdocApp$FooMock$7@380173a2


// Using the throws or throwing function
when(fooMock.increment).expects(5).throws(new RuntimeException("message"))
// res31: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(5) once (called once)
when(fooMock.increment).expects(6).throwing(new RuntimeException("message"))
// res32: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(6) once (never called - UNSATISFIED)

try {
    fooMock.increment(5)
    false
} catch {
    case _: RuntimeException => true
    case _ => false
}
// res33: Boolean = true


// Throwing in an onCall definition

when(fooMock.increment).expects(*).onCall { (i) =>
  if(i==0) throw new RuntimeException("i == 0") 
  else i + 1
}
// res34: CallHandler1[Int, Int] = <advanced-topics.md#L322> Foo.increment(*) once (called once)

try {
    fooMock.increment(0)
    false
} catch {
    case _: RuntimeException => true
    case _ => false
}
// res35: Boolean = true

Argument Capture

ScalaMock support capturing argument when using mocks. This allow the usage of MatchAny while asserting the argument after the fact.

Scala3Mock doesn't current support this feature, although nothing in the library actively prevent its inclusion. Contribution welcome.

See https://github.com/fmonniot/scala3mock/issues/5 if you'd like to help.

Mocking 0-parameter functions

Mocking methods which have zero parameters or are parameter-less are a bit different. The when macro take a reference to the function without applying it. In the case of parameterless functions, such reference is indiguishable from applying it. Zero-parameters function could theoritically be seen as different, but that could lead to confusion (and indeed, it was legal in Scala 2 and turned out to be confusing).

To go around this, you can simply wrap the method into a () => mock.method function:

trait Test {
  def parameterless: Int
  def zeroParameter(): Int
}

val m = mock[Test]
// m: Test = repl.MdocSession$MdocApp$TestMock$1@20d10dcb

when(() => m.zeroParameter()).expects().returns(1)
// res36: CallHandler0[Int] = <advanced-topics.md#L374> Test.zeroParameter() once (never called - UNSATISFIED)
when(() => m.parameterless).expects().returns(1)
// res37: CallHandler0[Int] = <advanced-topics.md#L374> Test.parameterless() once (never called - UNSATISFIED)
← Cats IntegrationFAQ →
  • Mocking overloaded, curried and polymorphic methods
    • Overloaded methods
    • Polymorphic methods
    • Curried methods
  • Methods with implicit parameters
    • Repeated parameters
  • Returning values (onCall)
  • Call count
  • Exceptions
  • Argument Capture
  • Mocking 0-parameter functions
scala3mock
Docs
Getting StartedFAQ
Copyright (c) 2022-2023 François Monniot