CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-scala-js--scalajs-test-interface

Scala.js version of the sbt testing interface that provides a standardized API for test frameworks to integrate with SBT and run tests in a Scala.js (JavaScript) environment

Pending
Overview
Eval results
Files

discovery.mddocs/

Test Discovery

Test discovery system using fingerprints to identify test classes through annotations or inheritance patterns, enabling automatic detection of test suites during SBT's discovery phase.

Capabilities

Fingerprint Base Interface

Base interface for test class identification patterns used during discovery.

/**
 * A way to identify test classes and/or modules that should be discovered.
 * Implementations may not rely on identity of Fingerprints since they are serialized between JS/JVM.
 */
trait Fingerprint

AnnotatedFingerprint

Identifies test classes or modules based on the presence of specific annotations.

/**
 * Indicates classes or modules with a specific annotation should be discovered as test classes.
 * The annotation can be on top-level methods or on the class/module itself.
 */
trait AnnotatedFingerprint extends Fingerprint {
  /**
   * Indicates whether modules with the annotation should be considered during discovery.
   * If a framework allows both classes and modules, return two different fingerprints:
   * one with isModule() = false and another with isModule() = true.
   * @return true for modules (singleton objects), false for classes
   */
  def isModule(): Boolean
  
  /**
   * The fully qualified name of the annotation that identifies test classes/modules.
   * @return annotation name for test identification
   */
  def annotationName(): String
}

Usage Examples:

// Discover classes with @Test annotation
val classAnnotationFingerprint = new AnnotatedFingerprint {
  def isModule() = false
  def annotationName() = "org.junit.Test"
}

// Discover objects with @TestSuite annotation  
val objectAnnotationFingerprint = new AnnotatedFingerprint {
  def isModule() = true
  def annotationName() = "com.example.TestSuite"
}

// Framework returning both fingerprints
class MyFramework extends Framework {
  def fingerprints(): Array[Fingerprint] = Array(
    classAnnotationFingerprint,
    objectAnnotationFingerprint
  )
}

SubclassFingerprint

Identifies test classes or modules based on inheritance from specific superclasses or traits.

/**
 * Indicates classes (and possibly modules) that extend a particular superclass
 * or mix in a particular supertrait should be discovered as test classes.
 */
trait SubclassFingerprint extends Fingerprint {
  /**
   * Indicates whether modules (singleton objects) that extend the superclass/supertrait
   * should be considered during discovery.
   * Returning false speeds up discovery by quickly bypassing module classes.
   * @return true to discover modules, false for classes only
   */
  def isModule(): Boolean
  
  /**
   * The name of the superclass or supertrait that identifies test classes.
   * @return superclass/supertrait name for test identification
   */
  def superclassName(): String
  
  /**
   * Indicates whether discovered classes must have a no-arg constructor.
   * If true, client should not discover subclasses without no-arg constructors.
   * @return true if no-arg constructor required
   */
  def requireNoArgConstructor(): Boolean
}

Usage Examples:

// Discover classes extending TestSuite (with no-arg constructor)
val suiteFingerprint = new SubclassFingerprint {
  def isModule() = false
  def superclassName() = "com.example.TestSuite"
  def requireNoArgConstructor() = true
}

// Discover objects extending SpecBase (no constructor requirement for objects)
val specFingerprint = new SubclassFingerprint {
  def isModule() = true
  def superclassName() = "com.example.SpecBase" 
  def requireNoArgConstructor() = false
}

// Discover both classes and objects extending BaseTest
val flexibleFingerprint = new SubclassFingerprint {
  def isModule() = false // or true for separate fingerprint
  def superclassName() = "com.example.BaseTest"
  def requireNoArgConstructor() = false
}

Discovery Process

Framework Fingerprint Registration

Test frameworks register their discovery patterns by returning fingerprints from the fingerprints() method:

class MyTestFramework extends Framework {
  def fingerprints(): Array[Fingerprint] = Array(
    // JUnit-style annotation discovery
    new AnnotatedFingerprint {
      def isModule() = false
      def annotationName() = "org.junit.Test"
    },
    
    // ScalaTest-style inheritance discovery
    new SubclassFingerprint {
      def isModule() = false
      def superclassName() = "org.scalatest.Suite"
      def requireNoArgConstructor() = true
    },
    
    // Specs2-style object discovery
    new SubclassFingerprint {
      def isModule() = true
      def superclassName() = "org.specs2.Specification"
      def requireNoArgConstructor() = false
    }
  )
}

Discovery Algorithm

SBT uses fingerprints to scan the classpath and identify test classes:

  1. Classpath Scanning: SBT scans compiled classes in test directories
  2. Fingerprint Matching: Each class/object checked against all registered fingerprints
  3. Annotation Checking: For AnnotatedFingerprint, check for specified annotations
  4. Inheritance Checking: For SubclassFingerprint, check superclass hierarchy
  5. Constructor Validation: If requireNoArgConstructor() is true, verify no-arg constructor exists
  6. TaskDef Creation: Create TaskDef instances for discovered test classes
  7. Task Creation: Pass TaskDef array to Runner.tasks() for task creation

Multiple Fingerprints

Frameworks can register multiple fingerprints to support different discovery patterns:

def fingerprints(): Array[Fingerprint] = Array(
  // Method-level annotations
  new AnnotatedFingerprint {
    def isModule() = false
    def annotationName() = "com.example.Test"
  },
  
  // Class-level annotations
  new AnnotatedFingerprint {
    def isModule() = false  
    def annotationName() = "com.example.TestClass"
  },
  
  // Inheritance-based discovery
  new SubclassFingerprint {
    def isModule() = false
    def superclassName() = "com.example.BaseTest"
    def requireNoArgConstructor() = true
  },
  
  // Object-based specs
  new SubclassFingerprint {
    def isModule() = true
    def superclassName() = "com.example.Spec"
    def requireNoArgConstructor() = false // Objects don't have constructors
  }
)

Discovery Best Practices

Performance Optimization:

  • Set isModule() = false for class-only frameworks to skip object scanning
  • Use requireNoArgConstructor() = true to filter classes early
  • Keep superclassName() and annotationName() as specific as possible

Flexibility:

  • Support both classes and objects by providing separate fingerprints
  • Consider annotation-based and inheritance-based patterns
  • Allow for different constructor requirements

Framework Compatibility:

  • Use standard annotation names when possible (e.g., JUnit annotations)
  • Follow common inheritance patterns (e.g., ScalaTest Suite)
  • Support both new and legacy test patterns
// Comprehensive discovery for flexible framework
class FlexibleFramework extends Framework {
  def fingerprints(): Array[Fingerprint] = Array(
    // JUnit compatibility
    new AnnotatedFingerprint {
      def isModule() = false
      def annotationName() = "org.junit.Test"
    },
    
    // Custom annotation support
    new AnnotatedFingerprint {
      def isModule() = false
      def annotationName() = "com.myframework.Test"
    },
    
    // Suite-based classes
    new SubclassFingerprint {
      def isModule() = false
      def superclassName() = "com.myframework.TestSuite"
      def requireNoArgConstructor() = true
    },
    
    // Spec-based objects
    new SubclassFingerprint {
      def isModule() = true
      def superclassName() = "com.myframework.Spec"
      def requireNoArgConstructor() = false
    }
  )
}

Error Handling

Discovery Failures

Discovery typically handles errors gracefully by skipping problematic classes:

// Framework should handle discovery errors internally
def fingerprints(): Array[Fingerprint] = {
  try {
    Array(
      createAnnotationFingerprint(),
      createSubclassFingerprint()
    )
  } catch {
    case _: ClassNotFoundException =>
      // Fallback to basic discovery
      Array(createBasicFingerprint())
  }
}

Validation

Fingerprints should validate their configuration:

class MyAnnotatedFingerprint(annotation: String) extends AnnotatedFingerprint {
  require(annotation != null && annotation.nonEmpty, "Annotation name cannot be empty")
  
  def annotationName() = annotation
  def isModule() = false
}

Install with Tessl CLI

npx tessl i tessl/maven-org-scala-js--scalajs-test-interface

docs

discovery.md

events.md

execution.md

framework.md

index.md

logging.md

tile.json