Search code examples
scalascalatest

Scala Testing of Trait Applied to Implementing Sub-Classes


Given a scala trait with implementing sub-classes

trait Foo {
  def doesStuff() : Int
}

case class Bar() extends Foo { ... }

case class Baz() extends Foo { ... }

How do the unit tests get organized in order to test the trait and then apply the tests to each of the implementations?

I'm looking for something of the form:

class FooSpec(foo : Foo) extends FlatSpec {
  "A Foo" should {
    "do stuff" in {
      assert(foo.doesStuff == 42)
    }
  }
}

Which would then be applied to each of the implementing classes:

FooSpec(Bar())

FooSpec(Baz())

Solution

  • If the implementations of Bar.doesStuff and Baz.doesStuff have different behavior, having two separate tests is the appropriate solution.

    import org.scalatest.FlatSpec
    
    class FooSpec1 extends FlatSpec {
    
      "a Bar" should "do a bar thing" in {
        Bar().doesStuff() == 42
      }
    
      "a Baz" should "do a baz thing" in {
        Baz().doesStuff() % 2 == 0
      }
    
    }
    

    However, if they have the same behavior, you can refactor the tests with a function to avoid duplicate code. I don't believe scalatest can achieve this reuse pattern at the spec level like you're asking for.

    import org.scalatest.FlatSpec
    
    class FooSpec2 extends FlatSpec {
    
      def checkDoesStuff(foo: Foo): Boolean =
        foo.doesStuff() == 42
    
      "a Bar" should "do a bar thing" in {
        checkDoesStuff(Bar())
      }
    
      "a Baz" should "do a baz thing" in {
        checkDoesStuff(Baz())
      }
    
    }
    

    Property-based testing can do exactly what you're looking for though. Here's an example using scalacheck:

    import org.scalacheck.{Gen, Properties}
    import org.scalacheck.Prop.forAll
    
    object FooProperties extends Properties("Foo"){
    
      val fooGen: Gen[Foo] = Gen.pick(1, List(Bar(), Baz())).map(_.head)
    
      property("a Foo always does stuff") = forAll(fooGen){
        (foo: Foo) => foo.doesStuff() == 42
      }
    
    }
    

    Unlike ScalaTest specs, properties are always functions. The forAll function takes a generator, samples values of the generator and runs the test on all samples. Our generator will always return either an instance of Bar or Baz which means the property will cover all the cases you're looking to test. forAll asserts that if a single test fails the entire property fails.