Skip to main content

Extras - Testing Tools Core

StubTools.missing

StubTools.missing is a tool for a stub (a simple function for testing) so that you don't need to use mock frameworks.

NOTE

In most cases, you probably don't want to use StubTools.missing directly. It is recommended to use StubToolsCats or StubToolsFx instead.

e.g.)

import cats.syntax.all._
import eu.timepit.refined.types.all._
import eu.timepit.refined.cats._
import eu.timepit.refined.auto._
import io.estatico.newtype.macros.newtype
import extras.testing.StubTools

object types {
@newtype case class Id(value: PosInt)
@newtype case class Name(value: NonEmptyString)
}
import types._

trait MyService {
def findName(id: Id): Option[Name]
}

object MyServiceStub {
def apply(f: => Option[Id => Option[Name]]): MyService = new MyService {
override def findName(id: Id): Option[Name] = f.fold[Option[Name]](throw StubTools.missing)(_(id))
}
}

class Hello(myService: MyService) {
def hello(id: Id): String = {
myService.findName(id)
.fold(s"No name found for id ${id.value}") { name =>
s"Hello ${name.value}"
}
}
}

val expectedId = Id(1)
// expectedId: Id = 1
val expectedName = Name("Kevin")
// expectedName: Name = Kevin

val myService: MyService = MyServiceStub(((id: Id) =>
if (id.value === expectedId.value)
expectedName.some
else
none
).some)
// myService: MyService = repl.MdocSession$MdocApp0$MyServiceStub$$anon$1@378b9449
val hello = new Hello(myService)
// hello: Hello = repl.MdocSession$MdocApp0$Hello@1a4fa03d
println(hello.hello(Id(1)))
// Hello Kevin
println(hello.hello(Id(2)))
// No name found for id 2
/* If you don't expect Hello to use MyService.findName,
* you can simply remove feeding the function for that operation
* and StubTools let you know where it fails if Hello uses MySerivce.findName.
*/
val myService2: MyService = MyServiceStub(none)
val hello2 = new Hello(myService2)
println(hello2.hello(Id(1)))
// extras.testing.StubTools$MissingStubException:
// >> Missing Stub implementation at
// >> repl.MdocSession$MdocApp0$MyServiceStub$$anon$1$$anonfun$findName$1.apply(testing-tools.md:45)
// >> ---
// >> Details:
// >> repl.MdocSession$MdocApp0$MyServiceStub$$anon$1$$anonfun$findName$1.apply(testing-tools.md:45)
// at repl.MdocSession$MdocApp0$MyServiceStub$$anon$1$$anonfun$findName$1.apply(testing-tools.md:45)
// at scala.Option.fold(Option.scala:263)
// at repl.MdocSession$MdocApp0$MyServiceStub$$anon$1.findName(testing-tools.md:45)
// at repl.MdocSession$MdocApp0$Hello.hello(testing-tools.md:52)
// at repl.MdocSession$MdocApp0$$anonfun$2.apply$mcV$sp(testing-tools.md:94)
// at repl.MdocSession$MdocApp0$$anonfun$2.apply(testing-tools.md:91)
// at repl.MdocSession$MdocApp0$$anonfun$2.apply(testing-tools.md:91)
// at mdoc.internal.document.DocumentBuilder$$doc$.crash(DocumentBuilder.scala:75)
// at repl.MdocSession$MdocApp0$.<clinit>(testing-tools.md:91)
// at repl.MdocSession$MdocApp.<init>(testing-tools.md:5)
// at repl.MdocSession$.app(testing-tools.md:3)
// at mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$2(DocumentBuilder.scala:89)
// at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
// at scala.util.DynamicVariable.withValue(DynamicVariable.scala:59)
// at scala.Console$.withErr(Console.scala:193)
// at mdoc.internal.document.DocumentBuilder$$doc$.$anonfun$build$1(DocumentBuilder.scala:89)
// at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.scala:18)
// at scala.util.DynamicVariable.withValue(DynamicVariable.scala:59)
// at scala.Console$.withOut(Console.scala:164)
// at mdoc.internal.document.DocumentBuilder$$doc$.build(DocumentBuilder.scala:88)
// at mdoc.internal.markdown.MarkdownBuilder$.$anonfun$buildDocument$2(MarkdownBuilder.scala:48)
// at mdoc.internal.markdown.MarkdownBuilder$$anon$1.run(MarkdownBuilder.scala:105)
//
NOTE

Why not just use mock framework for convenience? To answer that, please read Pitfalls of Mocking in tests from Xebia Functional (formerly known as 47 Degrees)

Besides what the blog tells you, mock frameworks often make you do bad practice like testing the implementation details with verify.

There is also an issue when your mock is not correctly set. You may get a NullPointerException for that, but it doesn't tell you where it's from and why you get it.