在Scalatest中实现工厂:Trait多实现契约测试复用问题
Great question! In ScalaTest, you can combine reflection-based implementation discovery with parameterized testing to automatically run your contract tests against all implementations of TraitToTest without manually specifying each one. Here's a step-by-step solution:
1. Build a Factory to Auto-Discover Implementations
First, create a helper object that uses Scala's reflection API to find all concrete classes implementing TraitToTest and instantiate them. This acts as your auto-discovery factory:
import scala.reflect.runtime.universe._ import scala.reflect.runtime.{currentMirror => cm} object TraitToTestFactory { def getAllImplementations: List[TraitToTest] = { // Get the type symbol for our trait val traitSymbol = typeOf[TraitToTest].typeSymbol // Filter for concrete classes that implement TraitToTest val concreteImplementations = cm.classes.filter { cls => cls.baseClasses.contains(traitSymbol) && !cls.isAbstract && // Skip the trait itself and abstract subclasses cls.fullName.startsWith("com.yourpackage.impl") // Optional: Restrict to your implementation package } // Try to instantiate each class (handles singletons and no-arg constructors) concreteImplementations.flatMap { cls => try { // First try to load as a singleton object (object Impl1 extends ...) val module = cm.staticModule(cls.fullName) Some(cm.reflectModule(module).instance.asInstanceOf[TraitToTest]) } catch { case _: Exception => // Fall back to instantiating via no-arg constructor (class Impl1 extends ...) try { val classSymbol = cls.asClass val constructor = classSymbol.primaryConstructor.asMethod val instance = cm.reflectClass(classSymbol).reflectConstructor(constructor)() Some(instance.asInstanceOf[TraitToTest]) } catch { case e: Exception => println(s"Skipping ${cls.fullName}: Failed to instantiate - ${e.getMessage}") None } } }.toList } }
2. Reuse Contract Tests with Parameterized Testing
Next, use ScalaTest's TableDrivenPropertyChecks to iterate over all discovered implementations and run your contract tests against each one. This ensures your test suite automatically includes new implementations as you add them:
import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers import org.scalatest.prop.TableDrivenPropertyChecks class TraitToTestContractSpec extends AnyFunSpec with Matchers with TableDrivenPropertyChecks { // Load all implementations from our factory val testImplementations = Table( "implementation", TraitToTestFactory.getAllImplementations: _* ) describe("TraitToTest general contracts") { // Run contract tests for every implementation forAll(testImplementations) { impl => val implName = impl.getClass.getSimpleName it(s"should satisfy contract 1 for $implName") { // Insert your contract 1 test logic here // Example: Verify no exceptions are thrown during doSome() noException should be thrownBy impl.doSome() } it(s"should satisfy contract 2 for $implName") { // Insert your contract 2 test logic here // Example: Add assertions specific to your trait's requirements } } } }
Key Notes & Optimizations
- Constructor Parameters: If your implementations require constructor arguments, adjust the factory to inject predefined test values (e.g., mock dependencies) or use a dependency injection framework to resolve instances.
- Test Isolation: Each implementation gets its own set of test cases, so failures in one won't affect others. The test report will clearly show which implementation failed which contract.
- Performance: Reflection can add minor overhead. For large codebases, consider caching the discovered implementations or using compile-time code generation (e.g., with macros) instead.
- Filtering: Tweak the
concreteImplementationsfilter to exclude test-only classes or internal implementations you don't want to validate.
内容的提问来源于stack exchange,提问作者Some Name

