Hi there!
I’ve been happily using Scala for a while now in a (mostly) purely functional environment. I use classes to model functions and compose them to create the application workflow.
What I’m consistently wondering is: What is the best way to test function composition in a pure way?
Here’s an attempt at illustrating the issue via a very simple example:
package com.example
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
private final class ExampleTest extends AnyFunSuite with Matchers {
/*
At some point we get to a leaf function - these are just there to show
the general implementation style...
In the real world it's reasonable to assume that they are more
complicated than the mock functions we'll use in the tests.
*/
class A extends (List[String] => String) {
override def apply(value: List[String]): String = ???
}
class B extends (String => Int) {
override def apply(value: String): Int = ???
}
// This is the function that we actually want to test. We use default
// values for production and supply mocks in tests.
// This is just an example. In reality, there can be more functions and arity and types usually vary...
class Composition(
a: List[String] => String = new A,
b: String => Int = new B
) extends (List[String] => Int) {
// andThen is an extremely simple example. Assume there is additional logic.
// The way a, b, ... are combined depends on the domain problem. The important thing is:
// They are intended to be used in a certain way to solve the problem.
override def apply(value: List[String]): Int = a.andThen(b)(value)
}
test("Composition should apply functions in correct order - v1") {
/*
Pure variant - create mock functions in a way that the data flows through
them and the result shows that each function has been passed in correct
order.
Unfortunately data and mock behavior is usually way more complex in real
world applications and mock functions become harder to write and equally
hard to understand.
In worst case scenarios, mocks start to emulate the production logic :(
Example: Using one field in a complex object and only changing that
field from mock to mock
*/
val testee = new Composition(a = _.mkString(" "), b = _.length)
testee.apply("hello" :: "world" :: "!" :: Nil) shouldBe 13
}
test("Composition should apply functions in correct order - v2") {
/*
Pure variant - but relying on the compiler type checks.
This way we can radically simplify the mocks and just return stub
values, checking that the last stub value is the overall result.
Assuming that null doesn't exist (we have outlawed the use of null
in our projects), since all types are distinct, there is only one
way the functions can be composed, which is backed by the compiler
type checking.
Is this reasonably safe? Or is this test insufficient?
Obviously this does not work when the types returned by the
functions are not distinct, e.g.
a: T => T
b: T => T
In that example we cannot verify that a has been called at all...
*/
val testee = new Composition(a = _ => "", b = _ => 42)
testee.apply("any" :: "input" :: Nil) shouldBe 42
}
test("Composition should apply functions in correct order - v3") {
/*
Impure variant using closures - manually check that each function
has been called with the expected input.
This is a little more work than the example above but ensures
everything is connected correctly - even when the types are
not distinct.
Unfortunately this implementation is impure :(
Is there a pure way to write this? Or is this a bad idea
in general?
*/
var actualInputA: List[String] = Nil
var actualInputB: String = ""
val testee = new Composition(
a = input => {
actualInputA = input
"input-b"
},
b = input => {
actualInputB = input
42
}
)
val input = "hello" :: "world" :: "!" :: Nil
testee.apply(input) shouldBe 42
actualInputA shouldEqual input
actualInputB shouldEqual "input-b"
}
}
Has anyone else considered these questions? How do you handle testing composition in a purely functional environment?
Edit: Formatting
Edit2: Added some comments for clarification