Case class definition change and tests


#1

I have a web service written in Scala. As an input it takes a JSON that gets deserialized to a case class with circe. Sometimes though the definition of some inputs change. I usually add new fields of type Option[T]. It all works ok after all. There are other projects that use definitions if these objects internally.

I have problems with my tests though. I have to add this new field in every constructor invocation I have. And this is painful. I thought about adding .zero value to the companion object of my model classes and then using .copy to instantiate object just with the fields I need. But this seems like an overkill.

Another way I see is to use default parameters in the constructor - but then it is possible to miss this parameter in production code. Is there some other way / pattern to do this? I feel like I am just missing something.

Cheers.


#2

You could add default parameters in a function on the companion object, calling the generated apply method, but with defaults and a different name, e.g. withDefaults:

case class Foo(a: Option[Int], b: Option[String] /*...*/)
object Foo {
  def withDefaults(a: Option[Int] = None, b: Option[String] = None) = Foo(a,b)
}

val foo = Foo.withDefaults(b = Some("hi"))

So basically a combination of your two approaches. I think this is cleaner than using .zero and .copy, and it also works if there is no zero value for some of your fields. It does not change the behaviour of the normal constructor, so your production code stays safe, and if you use that method to instantiate, its name makes clear, that there may be something set automatically.


#3

I would first write a sample test that should not change if the case class changes. Then I’d implement whatever is necessary to make it work. After all, it’s just a one-time cost–changes to the case class wouldn’t affect existing tests.

// MyServiceSpec.scala
import org.scalatest.refspec.RefSpec

class MyServiceSpec extends RefSpec {
  import MyServiceSpec._

  def `foo works` = assertResult("foo") {
    MyDomainType.Empty.withFoo("foo").foo.get
  }

  def `bar works` = assertResult("bar") {
    MyDomainType.Empty.withBar("bar").bar.get
  }
}

// MyDomainType.scala
case class MyDomainType(foo: Option[String], bar: Option[String]) {
  def withFoo(newFoo: String): MyDomainType = copy(foo = Some(newFoo))
  def withBar(newBar: String): MyDomainType = copy(bar = Some(newBar))
}

object MyDomainType {
  val Empty = MyDomainType(foo = None, bar = None)

  // JSON stuff...
}

If this seems like too much boilerplate, you might want to check out Monocle, you can use it to automate the withXXX methods.