org.scalatest.fixture.FixtureSuite.scala Maven / Gradle / Ivy
Show all versions of scalatest_2.8.0 Show documentation
/*
* Copyright 2001-2008 Artima, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.scalatest.fixture
import org.scalatest._
import collection.immutable.TreeSet
import java.lang.reflect.{InvocationTargetException, Method, Modifier}
import org.scalatest.Suite.checkForPublicNoArgConstructor
import org.scalatest.Suite.TestMethodPrefix
import org.scalatest.Suite.IgnoreAnnotation
import org.scalatest.Suite.InformerInParens
import FixtureSuite.FixtureAndInformerInParens
import FixtureSuite.FixtureInParens
import FixtureSuite.testMethodTakesAFixtureAndInformer
import FixtureSuite.testMethodTakesAnInformer
import FixtureSuite.testMethodTakesAFixture
import FixtureSuite.simpleNameForTest
import FixtureSuite.argsArrayForTestName
import org.scalatest.events._
import Suite.anErrorThatShouldCauseAnAbort
/**
* Suite that can pass a fixture object into its tests.
*
*
* This trait behaves similarly to trait org.scalatest.Suite, except that tests may have a fixture parameter. The type of the
* fixture parameter is defined by the abstract FixtureParam type, which is declared as a member of this trait.
* This trait also declares an abstract withFixture method. This withFixture method
* takes a OneArgTest, which is a nested trait defined as a member of this trait.
* OneArgTest has an apply method that takes a FixtureParam.
* This apply method is responsible for running a test.
* This trait's runTest method delegates the actual running of each test to withFixture, passing
* in the test code to run via the OneArgTest argument. The withFixture method (abstract in this trait) is responsible
* for creating the fixture argument and passing it to the test function.
*
*
*
* Subclasses of this trait must, therefore, do three things differently from a plain old org.scalatest.Suite:
*
*
*
* - define the type of the fixture parameter by specifying type
FixtureParam
* - define the
withFixture(OneArgTest) method
* - write test methods that take a fixture parameter (You can also define test methods that don't take a fixture parameter.)
*
*
*
* Here's an example:
*
*
*
* import org.scalatest.fixture.FixtureSuite
* import java.io.FileReader
* import java.io.FileWriter
* import java.io.File
*
* class MySuite extends FixtureSuite {
*
* // 1. define type FixtureParam
* type FixtureParam = FileReader
*
* // 2. define the withFixture method
* def withFixture(test: OneArgTest) {
*
* val FileName = "TempFile.txt"
*
* // Set up the temp file needed by the test
* val writer = new FileWriter(FileName)
* try {
* writer.write("Hello, test!")
* }
* finally {
* writer.close()
* }
*
* // Create the reader needed by the test
* val reader = new FileReader(FileName)
*
* try {
* // Run the test using the temp file
* test(reader)
* }
* finally {
* // Close and delete the temp file
* reader.close()
* val file = new File(FileName)
* file.delete()
* }
* }
*
* // 3. write test methods that take a fixture parameter
* def testReadingFromTheTempFile(reader: FileReader) {
* var builder = new StringBuilder
* var c = reader.read()
* while (c != -1) {
* builder.append(c.toChar)
* c = reader.read()
* }
* assert(builder.toString === "Hello, test!")
* }
*
* def testFirstCharOfTheTempFile(reader: FileReader) {
* assert(reader.read() === 'H')
* }
*
* // (You can also write tests methods that don't take a fixture parameter.)
* def testWithoutAFixture() {
* without fixture {
* assert(1 + 1 === 2)
* }
* }
* }
*
*
*
* If the fixture you want to pass into your tests consists of multiple objects, you will need to combine
* them into one object to use this trait. One good approach to passing multiple fixture objects is
* to encapsulate them in a tuple. Here's an example that takes the tuple approach:
*
*
*
* import org.scalatest.fixture.FixtureSuite
* import scala.collection.mutable.ListBuffer
*
* class MySuite extends FixtureSuite {
*
* type FixtureParam = (StringBuilder, ListBuffer[String])
*
* def withFixture(test: OneArgTest) {
*
* // Create needed mutable objects
* val stringBuilder = new StringBuilder("ScalaTest is ")
* val listBuffer = new ListBuffer[String]
*
* // Invoke the test function, passing in the mutable objects
* test(stringBuilder, listBuffer)
* }
*
* def testEasy(fixture: Fixture) {
* val (builder, buffer) = fixture
* builder.append("easy!")
* assert(builder.toString === "ScalaTest is easy!")
* assert(buffer.isEmpty)
* buffer += "sweet"
* }
*
* def testFun(fixture: Fixture) {
* val (builder, buffer) = fixture
* builder.append("fun!")
* assert(builder.toString === "ScalaTest is fun!")
* assert(buffer.isEmpty)
* }
* }
*
*
*
* When using a tuple to pass multiple fixture objects, it is usually helpful to give names to each
* individual object in the tuple with a pattern-match assignment, as is done at the beginning
* of each test method here with:
*
*
*
* val (builder, buffer) = fixture
*
*
*
* Another good approach to passing multiple fixture objects is
* to encapsulate them in a case class. Here's an example that takes the case class approach:
*
*
*
* import org.scalatest.fixture.FixtureSuite
* import scala.collection.mutable.ListBuffer
*
* class MySuite extends FixtureSuite {
*
* case class FixtureHolder(builder: StringBuilder, buffer: ListBuffer[String])
*
* type FixtureParam = FixtureHolder
*
* def withFixture(test: OneArgTest) {
*
* // Create needed mutable objects
* val stringBuilder = new StringBuilder("ScalaTest is ")
* val listBuffer = new ListBuffer[String]
*
* // Invoke the test function, passing in the mutable objects
* test(FixtureHolder(stringBuilder, listBuffer))
* }
*
* def testEasy(fixture: Fixture) {
* import fixture._
* builder.append("easy!")
* assert(builder.toString === "ScalaTest is easy!")
* assert(buffer.isEmpty)
* buffer += "sweet"
* }
*
* def testFun(fixture: Fixture) {
* fixture.builder.append("fun!")
* assert(fixture.builder.toString === "ScalaTest is fun!")
* assert(fixture.buffer.isEmpty)
* }
* }
*
*
*
* When using a case class to pass multiple fixture objects, it can be helpful to make the names of each
* individual object available as a single identifier with an import statement. This is the approach
* taken by the testEasy method in the previous example. Because it imports the members
* of the fixture object, the test method code can just use them as unqualified identifiers:
*
*
*
* def testEasy(fixture: Fixture) {
* import fixture._
* builder.append("easy!")
* assert(builder.toString === "ScalaTest is easy!")
* assert(buffer.isEmpty)
* buffer += "sweet"
* }
*
*
*
* Alternatively, you may sometimes prefer to qualify each use of a fixture object with the name
* of the fixture parameter. This approach, taken by the testFun method in the previous
* example, makes it more obvious which variables in your test method
* are part of the passed-in fixture:
*
*
*
* def testFun(fixture: Fixture) {
* fixture.builder.append("fun!")
* assert(fixture.builder.toString === "ScalaTest is fun!")
* assert(fixture.buffer.isEmpty)
* }
*
*
* Configuring fixtures and tests
*
*
* Sometimes you may want to write tests that are configurable. For example, you may want to write
* a suite of tests that each take an open temp file as a fixture, but whose file name is specified
* externally so that the file name can be can be changed from run to run. To accomplish this
* the OneArgTest trait has a configMap
* method, which will return a Map[String, Any] from which configuration information may be obtained.
* The runTest method of this trait will pass a OneArgTest to withFixture
* whose configMap method returns the configMap passed to runTest.
* Here's an example in which the name of a temp file is taken from the passed configMap:
*
*
*
* import org.scalatest.fixture.FixtureSuite
* import java.io.FileReader
* import java.io.FileWriter
* import java.io.File
*
* class MySuite extends FixtureSuite {
*
* type FixtureParam = FileReader
*
* def withFixture(test: OneArgTest) {
*
* require(
* test.configMap.contains("TempFileName"),
* "This suite requires a TempFileName to be passed in the configMap"
* )
*
* // Grab the file name from the configMap
* val FileName = test.configMap("TempFileName")
*
* // Set up the temp file needed by the test
* val writer = new FileWriter(FileName)
* try {
* writer.write("Hello, test!")
* }
* finally {
* writer.close()
* }
*
* // Create the reader needed by the test
* val reader = new FileReader(FileName)
*
* try {
* // Run the test using the temp file
* test(reader)
* }
* finally {
* // Close and delete the temp file
* reader.close()
* val file = new File(FileName)
* file.delete()
* }
* }
*
* def testReadingFromTheTempFile(reader: FileReader) {
* var builder = new StringBuilder
* var c = reader.read()
* while (c != -1) {
* builder.append(c.toChar)
* c = reader.read()
* }
* assert(builder.toString === "Hello, test!")
* }
*
* def testFirstCharOfTheTempFile(reader: FileReader) {
* assert(reader.read() === 'H')
* }
* }
*
*
*
* If you want to pass into each test the entire configMap that was passed to runTest, you
* can mix in trait ConfigMapFixture. See the documentation
* for ConfigMapFixture for the details, but here's a quick
* example of how it looks:
*
*
*
* import org.scalatest.fixture.FixtureSuite
* import org.scalatest.fixture.ConfigMapFixture
*
* class MySuite extends FixtureSuite with ConfigMapFixture {
*
* def testHello(configMap: Map[String, Any]) {
* // Use the configMap passed to runTest in the test
* assert(configMap.contains("hello")
* }
*
* def testWorld(configMap: Map[String, Any]) {
* assert(configMap.contains("world")
* }
* }
*
*
*
* Note: because a FixtureSuite's test methods are invoked with reflection at runtime, there is no good way to
* create a FixtureSuite containing test methods that take different fixtures. If you find you need to do this,
* you may want to split your class into multiple FixtureSuites, each of which contains test methods that take the
* common Fixture type defined in that class, or use a MultipleFixtureFunSuite.
*
*
* @author Bill Venners
*/
trait FixtureSuite extends org.scalatest.Suite { thisSuite =>
/**
* The type of the fixture parameter that can be passed into tests in this suite.
*/
protected type FixtureParam
/**
* Trait whose instances encapsulate a test function that takes a fixture and config map.
*
*
* The FixtureSuite trait's implementation of runTest passes instances of this trait
* to FixtureSuite's withFixture method, such as:
*
*
*
* def testSomething(fixture: Fixture) {
* // ...
* }
* def testSomethingElse(fixture: Fixture, info: Informer) {
* // ...
* }
*
*
*
* For more detail and examples, see the
* documentation for trait FixtureSuite.
*
*/
protected trait OneArgTest extends (FixtureParam => Unit) {
/**
* The name of this test.
*/
def name: String
/**
* Run the test, using the passed FixtureParam.
*/
def apply(fixture: FixtureParam)
/**
* Return a Map[String, Any] containing objects that can be used
* to configure the fixture and test.
*/
def configMap: Map[String, Any]
}
/*
* Trait whose instances encapsulate a test function that takes no fixture and config map.
*
*
* The FixtureSuite trait's implementation of runTest passes instances of this trait
* to FixtureSuite's withFixture method for test methods that take no
* fixture, such as:
*
*
*
* def testSomething() {
* // ...
* }
* def testSomethingElse(info: Informer) {
* // ...
* }
*
*
*
* This trait enables withFixture method implementatinos to detect test that
* don't require a fixture. If a fixture is expensive to create and cleanup, withFixture
* method implementations can opt to not create fixtures for tests that don't need them.
* For more detail and examples, see the
* documentation for trait FixtureSuite.
*
*/
/* protected trait FixturelessTest extends OneArgTest with (() => Unit) {
/**
* Run the test that takes no Fixture.
*/
def apply()
} */
/**
* Run the passed test function with a fixture created by this method.
*
*
* This method should create the fixture object needed by the tests of the
* current suite, invoke the test function (passing in the fixture object),
* and if needed, perform any clean up needed after the test completes.
* For more detail and examples, see the main documentation for this trait.
*
*
* @param fun the OneArgTest to invoke, passing in a fixture
*/
protected def withFixture(test: OneArgTest)
private[fixture] class TestFunAndConfigMap(val name: String, test: FixtureParam => Any, val configMap: Map[String, Any])
extends OneArgTest {
def apply(fixture: FixtureParam) {
test(fixture)
}
}
private[fixture] class FixturelessTestFunAndConfigMap(override val name: String, test: () => Any, override val configMap: Map[String, Any])
extends NoArgTest {
def apply() { test() }
}
// Need to override this one becaue it call getMethodForTestName
override def tags: Map[String, Set[String]] = {
def getTags(testName: String) =
/* AFTER THE DEPRECATION CYCLE FOR GROUPS TO TAGS (0.9.8), REPLACE THE FOLLOWING FOR LOOP WITH THIS COMMENTED OUT ONE
THAT MAKES SURE ANNOTATIONS ARE TAGGED WITH TagAnnotation.
for {
a <- getMethodForTestName(testName).getDeclaredAnnotations
annotationClass = a.annotationType
if annotationClass.isAnnotationPresent(classOf[TagAnnotation])
} yield annotationClass.getName
*/
for (a <- getMethodForTestName(testName).getDeclaredAnnotations)
yield a.annotationType.getName
val elements =
for (testName <- testNames; if !getTags(testName).isEmpty)
yield testName -> (Set() ++ getTags(testName))
Map() ++ elements
}
override def testNames: Set[String] = {
def takesInformer(m: Method) = {
val paramTypes = m.getParameterTypes
paramTypes.length == 1 && classOf[Informer].isAssignableFrom(paramTypes(0))
}
def takesTwoParamsOfTypesAnyAndInformer(m: Method) = {
val paramTypes = m.getParameterTypes
val hasTwoParams = paramTypes.length == 2
hasTwoParams && classOf[Informer].isAssignableFrom(paramTypes(1))
}
def takesOneParamOfAnyType(m: Method) = m.getParameterTypes.length == 1
def isTestMethod(m: Method) = {
val isInstanceMethod = !Modifier.isStatic(m.getModifiers())
// name must have at least 4 chars (minimum is "test")
val simpleName = m.getName
val firstFour = if (simpleName.length >= 4) simpleName.substring(0, 4) else ""
val paramTypes = m.getParameterTypes
val hasNoParams = paramTypes.length == 0
// Discover testNames(Informer) because if we didn't it might be confusing when someone
// actually wrote a testNames(Informer) method and it was silently ignored.
val isTestNames = simpleName == "testNames"
// Also, will discover both
// testNames(Object) and testNames(Object, Informer). Reason is if I didn't discover these
// it would likely just be silently ignored, and that might waste users' time
isInstanceMethod && (firstFour == "test") && ((hasNoParams && !isTestNames) ||
takesInformer(m) || takesOneParamOfAnyType(m) || takesTwoParamsOfTypesAnyAndInformer(m))
}
val testNameArray =
for (m <- getClass.getMethods; if isTestMethod(m)) yield
if (takesInformer(m))
m.getName + InformerInParens
else if (takesOneParamOfAnyType(m))
m.getName + FixtureInParens
else if (takesTwoParamsOfTypesAnyAndInformer(m))
m.getName + FixtureAndInformerInParens
else m.getName
TreeSet[String]() ++ testNameArray
}
protected override def runTest(testName: String, reporter: Reporter, stopper: Stopper, configMap: Map[String, Any], tracker: Tracker) {
if (testName == null || reporter == null || stopper == null || configMap == null || tracker == null)
throw new NullPointerException
val stopRequested = stopper
val report = wrapReporterIfNecessary(reporter)
val method = getMethodForTestName(testName)
// Create a Rerunner if the Suite has a no-arg constructor
val hasPublicNoArgConstructor = checkForPublicNoArgConstructor(getClass)
val rerunnable =
if (hasPublicNoArgConstructor)
Some(new TestRerunner(getClass.getName, testName))
else
None
val testStartTime = System.currentTimeMillis
report(TestStarting(tracker.nextOrdinal(), thisSuite.suiteName, Some(thisSuite.getClass.getName), testName, None, rerunnable))
try {
if (testMethodTakesAFixtureAndInformer(testName) || testMethodTakesAFixture(testName)) {
val testFun: FixtureParam => Unit = {
(fixture: FixtureParam) => {
val anyRefFixture: AnyRef = fixture.asInstanceOf[AnyRef] // TODO zap this cast
val args: Array[Object] =
if (testMethodTakesAFixtureAndInformer(testName)) {
val informer =
new Informer {
def apply(message: String) {
if (message == null)
throw new NullPointerException
report(InfoProvided(tracker.nextOrdinal(), message, Some(NameInfo(thisSuite.suiteName, Some(thisSuite.getClass.getName), Some(testName)))))
}
}
Array(anyRefFixture, informer)
}
else
Array(anyRefFixture)
method.invoke(thisSuite, args: _*)
}
}
withFixture(new TestFunAndConfigMap(testName, testFun, configMap))
}
else { // Test method does not take a fixture
val testFun: () => Unit = {
() => {
val args: Array[Object] =
if (testMethodTakesAnInformer(testName)) {
val informer =
new Informer {
def apply(message: String) {
if (message == null)
throw new NullPointerException
report(InfoProvided(tracker.nextOrdinal(), message, Some(NameInfo(thisSuite.suiteName, Some(thisSuite.getClass.getName), Some(testName)))))
}
}
Array(informer)
}
else
Array()
method.invoke(this, args: _*)
}
}
withFixture(new FixturelessTestFunAndConfigMap(testName, testFun, configMap))
}
val duration = System.currentTimeMillis - testStartTime
report(TestSucceeded(tracker.nextOrdinal(), thisSuite.suiteName, Some(thisSuite.getClass.getName), testName, Some(duration), None, rerunnable))
}
catch {
case ite: InvocationTargetException =>
val t = ite.getTargetException
t match {
case _: TestPendingException =>
report(TestPending(tracker.nextOrdinal(), thisSuite.suiteName, Some(thisSuite.getClass.getName), testName))
case e if !anErrorThatShouldCauseAnAbort(e) =>
val duration = System.currentTimeMillis - testStartTime
handleFailedTest(t, hasPublicNoArgConstructor, testName, rerunnable, report, tracker, duration)
case e => throw e
}
case e if !anErrorThatShouldCauseAnAbort(e) =>
val duration = System.currentTimeMillis - testStartTime
handleFailedTest(e, hasPublicNoArgConstructor, testName, rerunnable, report, tracker, duration)
case e => throw e
}
}
// TODO: This is identical with the one in Suite. Factor it out to an object somewhere.
private def handleFailedTest(throwable: Throwable, hasPublicNoArgConstructor: Boolean, testName: String,
rerunnable: Option[Rerunner], report: Reporter, tracker: Tracker, duration: Long) {
val message =
if (throwable.getMessage != null) // [bv: this could be factored out into a helper method]
throwable.getMessage
else
throwable.toString
report(TestFailed(tracker.nextOrdinal(), message, thisSuite.suiteName, Some(thisSuite.getClass.getName), testName, Some(throwable), Some(duration), None, rerunnable))
}
private def getMethodForTestName(testName: String) = {
val candidateMethods = getClass.getMethods.filter(_.getName == simpleNameForTest(testName))
val found =
if (testMethodTakesAFixtureAndInformer(testName))
candidateMethods.find(
candidateMethod => {
val paramTypes = candidateMethod.getParameterTypes
paramTypes.length == 2 && paramTypes(1) == classOf[Informer]
}
)
else if (testMethodTakesAnInformer(testName))
candidateMethods.find(
candidateMethod => {
val paramTypes = candidateMethod.getParameterTypes
paramTypes.length == 1 && paramTypes(0) == classOf[Informer]
}
)
else if (testMethodTakesAFixture(testName))
candidateMethods.find(
candidateMethod => {
val paramTypes = candidateMethod.getParameterTypes
paramTypes.length == 1
}
)
else
candidateMethods.find(_.getParameterTypes.length == 0)
found match {
case Some(method) => method
case None =>
throw new IllegalArgumentException(Resources("testNotFound", testName))
}
}
/*
/*
* Object that encapsulates a test function, which does not take a fixture,
* and a config map.
*
*
* The FixtureSuite trait's implementation of runTest passes instances of this trait
* to FixtureSuite's withFixture method for tests that do not require a fixture to
* be passed. For more detail and examples, see the
* documentation for trait FixtureSuite.
*
*/
protected trait NoArgTestFunction extends (FixtureParam => Any) {
/**
* Run the test, ignoring the passed Fixture.
*
*
* This traits implementation of this method invokes the overloaded form
* of apply that takes no parameters.
*
*/
final def apply(fixture: Fixture): Any = {
apply()
}
/**
* Run the test without a Fixture.
*/
def apply()
}
protected class WithoutWord {
def fixture(fun: => Any): NoArgTestFunction = {
new NoArgTestFunction {
def apply() { fun }
}
}
}
protected def without = new WithoutWord */
}
private object FixtureSuite {
val FixtureAndInformerInParens = "(FixtureParam, Informer)"
val FixtureInParens = "(FixtureParam)"
private def testMethodTakesAFixtureAndInformer(testName: String) = testName.endsWith(FixtureAndInformerInParens)
private def testMethodTakesAnInformer(testName: String) = testName.endsWith(InformerInParens)
private def testMethodTakesAFixture(testName: String) = testName.endsWith(FixtureInParens)
private def simpleNameForTest(testName: String) =
if (testName.endsWith(FixtureAndInformerInParens))
testName.substring(0, testName.length - FixtureAndInformerInParens.length)
else if (testName.endsWith(FixtureInParens))
testName.substring(0, testName.length - FixtureInParens.length)
else if (testName.endsWith(InformerInParens))
testName.substring(0, testName.length - InformerInParens.length)
else
testName
private def argsArrayForTestName(testName: String): Array[Class[_]] =
if (testMethodTakesAFixtureAndInformer(testName))
Array(classOf[Object], classOf[Informer])
else
Array(classOf[Informer])
}