Consider the following code. Animals should have a unique ID. I want to dynamically verify (in a test) that all concrete subtypes of Animal have unique IDs. I want my test to fail if, for example, Cat and Fish both try to pass in uniqueId
= 2.
sealed trait Animal {
val uniqueId: Int
}
abstract class Mammal(val uniqueId: Int) extends Animal
abstract class Fish(val uniqueId: Int) extends Animal
case class Dog(age: Int, name: String) extends Mammal(1)
case class Cat(favouriteFood: String) extends Mammal(2)
case class Salmon(isCute: Boolean) extends Fish(3)
I'm using reflections to get the classes.
import org.reflections.Reflections
val reflections = new Reflections("package.blah")
val allSubtypes: Seq[Class[_ <: Animal]] = reflections.getSubTypesOf(classOf[Animal]).asScala.toList
val concreteSubtypes: Seq[Class[_ <: Animal]] = allSubtypes.filter(c => !Modifier.isAbstract(c.getModifiers))
I'm starting to suspect it might not be possible but would love to be wrong! I have the classes but no way to instantiate instances of them because all the constructors differ and I'm not sure if I can access the uniqueId
from just the class.
As Tomer Shetah's comment says, this doesn't seem to be possible as stated. But it may work if you can split Animal
and AnimalCompanion
, and make the id actually unique per AnimalCompanion
:
abstract class AnimalCompanion(val uniqueId: Int)
sealed trait Animal {
def companion: AnimalCompanion
def uniqueId = companion.uniqueId
}
abstract class Mammal(val companion: AnimalCompanion) extends Animal
abstract class Fish(val companion: AnimalCompanion) extends Animal
case class Dog(age: Int, name: String) extends Mammal(Dog)
object Dog extends AnimalCompanion(1)
case class Cat(favouriteFood: String) extends Mammal(Cat)
object Cat extends AnimalCompanion(2)
case class Salmon(isCute: Boolean) extends Fish(Salmon)
object Salmon extends AnimalCompanion(3)
Then you find all subtypes of AnimalCompanion
in the same way, and because they are all object
s they'll have the MODULE$
field holding the instance and that instance will have the uniqueId
field and getter method.
Without testing, something like this should work:
val allCompanionClasses = reflections.getSubTypesOf(classOf[AnimalCompanion]).asScala
val allCompanionInstances = allCompanionClasses.map(_.getField("MODULE$").get(null))
val allUniqueIds = allCompanionInstances.map(x => x.getClass().getMethod("uniqueId").invoke(x))
and then it remains to check they are actually unique e.g. by
allUniqueIds.toSet.size == allUniqueIds.size
Without changing the structure, I'd consider a different approach: provide the instances yourself, just verify they include all classes.
val instances = Set(Dog(0, ""), Cat(""), Salmon(true))
// allSubtypes defined in the question
assert(allSubtypes.toSet == instances.map(_.getClass))
// same logic as above but we already have a Set
assert(instances.map(_.uniqueId).size == instances.size)