All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.scalatest.funspec.PathAnyFunSpec.scala Maven / Gradle / Ivy

/*
 * Copyright 2001-2019 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.funspec

import org.scalatest._

/**
 * A sister class to org.scalatest.funspec.AnyFunSpec that isolates tests by running each test in its own
 * instance of the test class, and for each test, only executing the path leading to that test.
 *
 * 

* Class PathAnyFunSpec behaves similarly to class org.scalatest.funspec.AnyFunSpec, except that tests * are isolated based on their path. The purpose of PathAnyFunSpec is to facilitate writing * specification-style tests for mutable objects in a clear, boilerpate-free way. To test mutable objects, you need to * mutate them. Using a path class, you can make a statement in text, then implement that statement in code (including * mutating state), and nest and combine these test/code pairs in any way you wish. Each test will only see * the side effects of code that is in blocks that enclose the test. Here's an example: *

* *
 * import org.scalatest.funspec
 * import org.scalatest.matchers.should.Matchers
 * import scala.collection.mutable.ListBuffer
 *
 * class ExampleSpec extends funspec.PathAnyFunSpec with Matchers {
 *
 *   describe("A ListBuffer") {
 *
 *     val buf = ListBuffer.empty[Int] // This implements "A ListBuffer"
 *
 *     it("should be empty when created") {
 *
 *       // This test sees:
 *       //   val buf = ListBuffer.empty[Int]
 *       // So buf is: ListBuffer()
 *
 *       buf should be ('empty)
 *     }
 *
 *     describe("when 1 is appended") {
 *
 *       buf += 1 // This implements "when 1 is appended", etc...
 *
 *       it("should contain 1") {
 *
 *         // This test sees:
 *         //   val buf = ListBuffer.empty[Int]
 *         //   buf += 1
 *         // So buf is: ListBuffer(1)
 *
 *         buf.remove(0) should equal (1)
 *         buf should be ('empty)
 *       }
 *
 *       describe("when 2 is appended") {
 *
 *         buf += 2
 *
 *         it("should contain 1 and 2") {
 *
 *           // This test sees:
 *           //   val buf = ListBuffer.empty[Int]
 *           //   buf += 1
 *           //   buf += 2
 *           // So buf is: ListBuffer(1, 2)
 *
 *           buf.remove(0) should equal (1)
 *           buf.remove(0) should equal (2)
 *           buf should be ('empty)
 *         }
 *
 *         describe("when 2 is removed") {
 *
 *           buf -= 2
 *
 *           it("should contain only 1 again") {
 *
 *             // This test sees:
 *             //   val buf = ListBuffer.empty[Int]
 *             //   buf += 1
 *             //   buf += 2
 *             //   buf -= 2
 *             // So buf is: ListBuffer(1)
 *
 *             buf.remove(0) should equal (1)
 *             buf should be ('empty)
 *           }
 *         }
 *
 *         describe("when 3 is appended") {
 *
 *           buf += 3
 *
 *           it("should contain 1, 2, and 3") {
 *
 *             // This test sees:
 *             //   val buf = ListBuffer.empty[Int]
 *             //   buf += 1
 *             //   buf += 2
 *             //   buf += 3
 *             // So buf is: ListBuffer(1, 2, 3)
 *
 *             buf.remove(0) should equal (1)
 *             buf.remove(0) should equal (2)
 *             buf.remove(0) should equal (3)
 *             buf should be ('empty)
 *           }
 *         }
 *       }
 *
 *       describe("when 88 is appended") {
 *
 *         buf += 88
 *
 *         it("should contain 1 and 88") {
 *
 *           // This test sees:
 *           //   val buf = ListBuffer.empty[Int]
 *           //   buf += 1
 *           //   buf += 88
 *           // So buf is: ListBuffer(1, 88)
 *
 *           buf.remove(0) should equal (1)
 *           buf.remove(0) should equal (88)
 *           buf should be ('empty)
 *         }
 *       }
 *     }
 *
 *     it("should have size 0 when created") {
 *
 *       // This test sees:
 *       //   val buf = ListBuffer.empty[Int]
 *       // So buf is: ListBuffer()
 *
 *       buf should have size 0
 *     }
 *   }
 * }
 * 
* *

* Note that the above class is organized by writing a bit of specification text that opens a new block followed * by, at the top of the new block, some code that "implements" or "performs" what is described in the text. This is repeated as * the mutable object (here, a ListBuffer), is prepared for the enclosed tests. For example: *

* *

 * describe("A ListBuffer") {
 *   val buf = ListBuffer.empty[Int]
 * 
* *

* Or: *

* *
 * describe("when 2 is appended") {
 *   buf += 2
 * 
* *

* Note also that although each test mutates the ListBuffer, none of the other tests observe those * side effects: *

* *

 * it("should contain 1") {
 *
 *   buf.remove(0) should equal (1)
 *   // ...
 * }
 *
 * describe("when 2 is appended") {
 *
 *   buf += 2
 *
 *   it("should contain 1 and 2") {
 *
 *     // This test does not see the buf.remove(0) from the previous test,
 *     // so the first element in the ListBuffer is again 1
 *     buf.remove(0) should equal (1)
 *     buf.remove(0) should equal (2)
 * 
* *

* This kind of isolation of tests from each other is a consequence of running each test in its own instance of the test * class, and can also be achieved by simply mixing OneInstancePerTest into a regular * org.scalatest.funspec.AnyFunSpec. However, PathAnyFunSpec takes isolation one step further: a test * in a PathAnyFunSpec does not observe side effects performed outside tests in earlier blocks that do not * enclose it. Here's an example: *

* *
 * describe("when 2 is removed") {
 *
 *   buf -= 2
 *
 *   // ...
 * }
 *
 * describe("when 3 is appended") {
 *
 *   buf += 3
 *
 *   it("should contain 1, 2, and 3") {
 *
 *     // This test does not see the buf -= 2 from the earlier "when 2 is removed" block,
 *     // because that block does not enclose this test, so the second element in the
 *     // ListBuffer is still 2
 *     buf.remove(0) should equal (1)
 *     buf.remove(0) should equal (2)
 *     buf.remove(0) should equal (3)
 * 
* *

* Running the full ExampleSpec, shown above, in the Scala interpeter would give you: *

* *
 * scala> import org.scalatest._
 * import org.scalatest._
 *
 * scala> run(new ExampleSpec)
 * ExampleSpec:
 * A ListBuffer
 * - should be empty when created
 *   when 1 is appended
 *   - should contain 1
 *     when 2 is appended
 *     - should contain 1 and 2
 *       when 2 is removed
 *       - should contain only 1 again
 *       when 3 is appended
 *       - should contain 1, 2, and 3
 *     when 88 is appended
 *     - should contain 1 and 88
 * - should have size 0 when created
 * 
* *

* Note: class PathAnyFunSpec's approach to isolation was inspired in part by the * specsy framework, written by Esko Luontola. *

* *

Shared fixtures

* *

* A test fixture is objects or other artifacts (such as files, sockets, database * connections, etc.) used by tests to do their work. * If a fixture is used by only one test, then the definitions of the fixture objects can * be local to the method. If multiple tests need to share an immutable fixture, you can simply * assign them to instance variables. If multiple tests need to share mutable fixture objects or vars, * there's one and only one way to do it in a PathAnyFunSpec: place the mutable objects lexically before * the test. Any mutations needed by the test must be placed lexically before and/or after the test. * As used here, "Lexically before" means that the code needs to be executed during construction of that test's * instance of the test class to reach the test (or put another way, the * code is along the "path to the test.") "Lexically after" means that the code needs to be executed to exit the * constructor after the test has been executed. *

* *

* The reason lexical placement is the one and only one way to share fixtures in a PathAnyFunSpec is because * all of its lifecycle methods are overridden and declared final. Thus you can't mix in BeforeAndAfter or * BeforeAndAfterEach, because both override runTest, which is final in * a PathAnyFunSpec. You also can't override withFixture, because PathAnyFunSpec * extends Suite not TestSuite, * where withFixture is defined. In short: *

* *

*

* * * *
* In a PathAnyFunSpec, if you need some code to execute before a test, place that code lexically before * the test. If you need some code to execute after a test, place that code lexically after the test. *
*

* *

* The reason the life cycle methods are final, by the way, is to prevent users from attempting to combine * a PathAnyFunSpec's approach to isolation with other ways ScalaTest provides to share fixtures or * execute tests, because doing so could make the resulting test code hard to reason about. A * PathAnyFunSpec's execution model is a bit magical, but because it executes in one and only one * way, users should be able to reason about the code. * To help you visualize how a PathAnyFunSpec is executed, consider the following variant of * ExampleSpec that includes print statements: *

* *
 * import org.scalatest.funspec
 * import org.scalatest.matchers.Matchers
 * import scala.collection.mutable.ListBuffer
 *
 * class ExampleSpec extends funspec.PathAnyFunSpec with Matchers {
 *
 *   println("Start of: ExampleSpec")
 *   describe("A ListBuffer") {
 *
 *     println("Start of: A ListBuffer")
 *     val buf = ListBuffer.empty[Int]
 *
 *     it("should be empty when created") {
 *
 *       println("In test: should be empty when created; buf is: " + buf)
 *       buf should be ('empty)
 *     }
 *
 *     describe("when 1 is appended") {
 *
 *       println("Start of: when 1 is appended")
 *       buf += 1
 *
 *       it("should contain 1") {
 *
 *         println("In test: should contain 1; buf is: " + buf)
 *         buf.remove(0) should equal (1)
 *         buf should be ('empty)
 *       }
 *
 *       describe("when 2 is appended") {
 *
 *         println("Start of: when 2 is appended")
 *         buf += 2
 *
 *         it("should contain 1 and 2") {
 *
 *           println("In test: should contain 1 and 2; buf is: " + buf)
 *           buf.remove(0) should equal (1)
 *           buf.remove(0) should equal (2)
 *           buf should be ('empty)
 *         }
 *
 *         describe("when 2 is removed") {
 *
 *           println("Start of: when 2 is removed")
 *           buf -= 2
 *
 *           it("should contain only 1 again") {
 *
 *             println("In test: should contain only 1 again; buf is: " + buf)
 *             buf.remove(0) should equal (1)
 *             buf should be ('empty)
 *           }
 *
 *           println("End of: when 2 is removed")
 *         }
 *
 *         describe("when 3 is appended") {
 *
 *           println("Start of: when 3 is appended")
 *           buf += 3
 *
 *           it("should contain 1, 2, and 3") {
 *
 *             println("In test: should contain 1, 2, and 3; buf is: " + buf)
 *             buf.remove(0) should equal (1)
 *             buf.remove(0) should equal (2)
 *             buf.remove(0) should equal (3)
 *             buf should be ('empty)
 *           }
 *           println("End of: when 3 is appended")
 *         }
 *
 *         println("End of: when 2 is appended")
 *       }
 *
 *       describe("when 88 is appended") {
 *
 *         println("Start of: when 88 is appended")
 *         buf += 88
 *
 *         it("should contain 1 and 88") {
 *
 *           println("In test: should contain 1 and 88; buf is: " + buf)
 *           buf.remove(0) should equal (1)
 *           buf.remove(0) should equal (88)
 *           buf should be ('empty)
 *         }
 *
 *         println("End of: when 88 is appended")
 *       }
 *
 *       println("End of: when 1 is appended")
 *     }
 *
 *     it("should have size 0 when created") {
 *
 *       println("In test: should have size 0 when created; buf is: " + buf)
 *       buf should have size 0
 *     }
 *
 *     println("End of: A ListBuffer")
 *   }
 *   println("End of: ExampleSpec")
 *   println()
 * }
 * 
* *

* Running the above version of ExampleSpec in the Scala interpreter will give you output similar to: *

* *
 * scala> import org.scalatest._
 * import org.scalatest._
 *
 * scala> run(new ExampleSpec)
 * ExampleSpec:
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * In test: should be empty when created; buf is: ListBuffer()
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * Start of: when 1 is appended
 * In test: should contain 1; buf is: ListBuffer(1)
 * ExampleSpec:
 * End of: when 1 is appended
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * Start of: when 1 is appended
 * Start of: when 2 is appended
 * In test: should contain 1 and 2; buf is: ListBuffer(1, 2)
 * End of: when 2 is appended
 * End of: when 1 is appended
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * Start of: when 1 is appended
 * Start of: when 2 is appended
 * Start of: when 2 is removed
 * In test: should contain only 1 again; buf is: ListBuffer(1)
 * End of: when 2 is removed
 * End of: when 2 is appended
 * End of: when 1 is appended
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * Start of: when 1 is appended
 * Start of: when 2 is appended
 * Start of: when 3 is appended
 * In test: should contain 1, 2, and 3; buf is: ListBuffer(1, 2, 3)
 * End of: when 3 is appended
 * End of: when 2 is appended
 * End of: when 1 is appended
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * Start of: when 1 is appended
 * Start of: when 88 is appended
 * In test: should contain 1 and 88; buf is: ListBuffer(1, 88)
 * End of: when 88 is appended
 * End of: when 1 is appended
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * Start of: ExampleSpec
 * Start of: A ListBuffer
 * In test: should have size 0 when created; buf is: ListBuffer()
 * End of: A ListBuffer
 * End of: ExampleSpec
 *
 * A ListBuffer
 * - should be empty when created
 *   when 1 is appended
 *   - should contain 1
 *     when 2 is appended
 *     - should contain 1 and 2
 *       when 2 is removed
 *       - should contain only 1 again
 *       when 3 is appended
 *       - should contain 1, 2, and 3
 *     when 88 is appended
 *     - should contain 1 and 88
 * - should have size 0 when created
 * 
* *

* Note that each test is executed in order of appearance in the PathAnyFunSpec, and that only * those println statements residing in blocks that enclose the test being run are executed. Any * println statements in blocks that do not form the "path" to a test are not executed in the * instance of the class that executes that test. *

* * *

How it executes

* *

* To provide its special brand of test isolation, PathAnyFunSpec executes quite differently from its * sister class in org.scalatest.funspec. An org.scalatest.funspec.AnyFunSpec * registers tests during construction and executes them when run is invoked. An * org.scalatest.funspec.PathAnyFunSpec, by contrast, runs each test in its own instance while that * instance is being constructed. During construction, it registers not the tests to run, but the results of * running those tests. When run is invoked on a PathAnyFunSpec, it reports the registered * results and does not run the tests again. If run is invoked a second or third time, in fact, * a PathAnyFunSpec will each time report the same results registered during construction. If you want * to run the tests of a PathAnyFunSpec anew, you'll need to create a new instance and invoke * run on that. *

* *

* A PathAnyFunSpec will create one instance for each "leaf" node it contains. The main kind of leaf node is * a test, such as: *

* *
 * // One instance will be created for each test
 * it("should be empty when created") {
 *   buf should be ('empty)
 * }
 * 
* *

* However, an empty scope (a scope that contains no tests or nested scopes) is also a leaf node: *

* *
 *  // One instance will be created for each empty scope
 * describe("when 99 is added") {
 *   // A scope is "empty" and therefore a leaf node if it has no
 *   // tests or nested scopes, though it may have other code (which
 *   // will be executed in the instance created for that leaf node)
 *   buf += 99
 * }
 * 
* *

* The tests will be executed sequentially, in the order of appearance. The first test (or empty scope, * if that is first) will be executed when a class that mixes in PathAnyFunSpec is * instantiated. Only the first test will be executed during this initial instance, and of course, only * the path to that test. Then, the first time the client uses the initial instance (by invoking one of run, * expectedTestsCount, tags, or testNames on the instance), the initial instance will, * before doing anything else, ensure that any remaining tests are executed, each in its own instance. *

* *

* To ensure that the correct path is taken in each instance, and to register its test results, the initial * PathAnyFunSpec instance must communicate with the other instances it creates for running any subsequent * leaf nodes. It does so by setting a thread-local variable prior to creating each instance (a technique * suggested by Esko Luontola). Each instance * of PathAnyFunSpec checks the thread-local variable. If the thread-local is not set, it knows it * is an initial instance and therefore executes every block it encounters until it discovers, and executes the * first test (or empty scope, if that's the first leaf node). It then discovers, but does not execute the next * leaf node, or discovers there are no other leaf nodes remaining to execute. It communicates the path to the next * leaf node, if any, and the result of running the test it did execute, if any, back to the initial instance. The * initial instance repeats this process until all leaf nodes have been executed and all test results registered. *

* * *

Ignored tests

* *

* You mark a test as ignored in an org.scalatest.funspec.PathAnyFunSpec in the same manner as in * an org.scalatest.funspec.AnyFunSpec. Please see the Ignored tests section * in its documentation for more information. *

* *

* Note that a separate instance will be created for an ignored test, * and the path to the ignored test will be executed in that instance, but the test function itself will not * be executed. Instead, a TestIgnored event will be fired. *

* * *

Informers

* *

* You output information using Informers in an org.scalatest.funspec.PathAnyFunSpec in the same manner * as in an org.scalatest.funspec.AnyFunSpec. Please see the Informers * section in its documentation for more information. *

* * *

Pending tests

* *

* You mark a test as pending in an org.scalatest.funspec.PathAnyFunSpec in the same manner as in * an org.scalatest.funspec.AnyFunSpec. Please see the Pending tests * section in its documentation for more information. *

* *

* Note that a separate instance will be created for a pending test, * and the path to the ignored test will be executed in that instance, as well as the test function (up until it * completes abruptly with a TestPendingException). *

* * *

Tagging tests

* *

* You can place tests into groups by tagging them in an org.scalatest.funspec.PathAnyFunSpec in the same manner * as in an org.scalatest.funspec.AnyFunSpec. Please see the Tagging tests * section in its documentation for more information. *

* *

* Note that one difference between this class and its sister class * org.scalatest.funspec.AnyFunSpec is that because tests are executed at construction time, rather than each * time run is invoked, an org.scalatest.funspec.PathAnyFunSpec will always execute all non-ignored tests. When * run is invoked on a PathAnyFunSpec, if some tests are excluded based on tags, the registered * results of running those tests will not be reported. (But those tests will have already run and the results * registered.) By contrast, because an org.scalatest.funspec.AnyFunSpec only executes tests after run * has been called, and at that time the tags to include and exclude are known, only tests selected by the tags * will be executed. *

* *

* In short, in an org.scalatest.funspec.AnyFunSpec, tests not selected by the tags to include * and exclude specified for the run (via the Filter passed to run) will not be executed. * In an org.scalatest.funspec.PathAnyFunSpec, by contrast, all non-ignored tests will be executed, each * during the construction of its own instance, and tests not selected by the tags to include and exclude specified * for a run will not be reported. (One upshot of this is that if you have tests that you want to tag as being slow so * you can sometimes exclude them during a run, you probably don't want to put them in a PathAnyFunSpec. Because * in a path.Freespec the slow tests will be run regardless, with only their registered results not being reported * if you exclude slow tests during a run.) *

* *

Shared tests

*

* You can factor out shared tests in an org.scalatest.funspec.PathAnyFunSpec in the same manner as in * an org.scalatest.funspec.AnyFunSpec. Please see the Shared tests * section in its documentation for more information. *

* *

Nested suites

* *

* Nested suites are not allowed in a PathAnyFunSpec. Because * a PathAnyFunSpec executes tests eagerly at construction time, registering the results of those test runs * and reporting them later when run is invoked, the order of nested suites versus test runs would be * different in a org.scalatest.funspec.PathAnyFunSpec than in an org.scalatest.funspec.AnyFunSpec. In * org.scalatest.funspec.AnyFunSpec's implementation of run, nested suites are executed then tests * are executed. A org.scalatest.funspec.PathAnyFunSpec with nested suites would execute these in the opposite * order: first tests then nested suites. To help make PathAnyFunSpec code easier to * reason about by giving readers of one less difference to think about, nested suites are not allowed. If you want * to add nested suites to a PathAnyFunSpec, you can instead wrap them all in a * Suites object. They will * be executed in the order of appearance (unless a Distributor is passed, in which case * they will execute in parallel). *

*

* *

Durations

*

* Many ScalaTest events include a duration that indicates how long the event being reported took to execute. For * example, a TestSucceeded event provides a duration indicating how long it took for that test * to execute. A SuiteCompleted event provides a duration indicating how long it took for that entire * suite of tests to execute. *

* *

* In the test completion events fired by a PathAnyFunSpec (TestSucceeded, * TestFailed, or TestPending), the durations reported refer * to the time it took for the tests to run. This time is registered with the test results and reported along * with the test results each time run is invoked. * By contrast, the suite completion events fired for a PathAnyFunSpec represent the amount of time * it took to report the registered results. (These events are not fired by PathAnyFunSpec, but instead * by the entity that invokes run on the PathAnyFunSpec.) As a result, the total time * for running the tests of a PathAnyFunSpec, calculated by summing the durations of all the individual * test completion events, may be greater than the duration reported for executing the entire suite. *

* * @author Bill Venners * @author Chua Chee Seng */ @Finders(Array("org.scalatest.finders.FunSpecFinder")) // SKIP-SCALATESTJS,NATIVE-START class PathAnyFunSpec extends org.scalatest.funspec.PathAnyFunSpecLike { // SKIP-SCALATESTJS,NATIVE-END //SCALATESTJS,NATIVE-ONLY abstract class PathAnyFunSpec extends org.scalatest.funspec.PathAnyFunSpecLike { /** * Returns a user friendly string for this suite, composed of the * simple name of the class (possibly simplified further by removing dollar signs if added by the Scala interpeter) and, if this suite * contains nested suites, the result of invoking toString on each * of the nested suites, separated by commas and surrounded by parentheses. * * @return a user-friendly string for this suite */ override def toString: String = Suite.suiteToString(None, this) }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy