PackageDescription: SUnit

S Unit

Last published: July 25, 2014 by 'nross'

Defines 5 Classes
Extends 5 Classes

SUnit is the original test framework that now has so many *Unit imitators. It powered test-driven development in the eXtreme Programming method, inspirer of all agile methods. The current SUnit is little changed from the original but some features have been added: additions to the assertion protocol, additional class TestResource and abstract superclass TestAsserter, and basic logging.

Pluggable TestSuites and TestResults
Users of SUnit mainly subclass TestCase. Recent versions make it easier to subclass TestSuite and/or TestResult in the more limited range of cases where that can be useful. Use TestCase class>>suiteClass: to plug in a more specific TestSuite subclass if desired. During a run, behaviour is principally on the TestSuite (and the tests), while data is captured on the TestResult, so the suggested specialisation strategy is to override #run (and/or other methods, but see the comment in TestSuite>>run) in a specific TestSuite subclass to effect the behaviour required - e.g. to capture the duration of the run, or the state of TestResources before all are reset at the end - and to add this data to a generic test result subclass intended to work with several such specific suite classes. For an example of such a generic result class, see ClassifiedTestResult in SUnitResourcePatterns. For an example of how TestResult subclasses may be plugged in and written by tests instead of, or as well as, the above, see TestSkip and references in that utility and in SUnitResourcePatternTests.

Assertion description strings
Make your tests more informative by using #assert:description: and #deny:description: rather than just #assert and #deny. These methods take a string as their second argument. If the test case fails, this string will be shown in the notifier (and in the log, if logging is active) instead of the less informative default: 'Assertion failed'. Writing description strings can also be an efficient way to document the intent of an assertion and to express what its failure would mean both in generic convenience protocol, e.g.

TestAsserter>>assert: anObject equals: anotherObject
self assert: anObject = anotherObject
description: anObject printString, ' is not equal to ', anotherObject printString.

and in specific tests, e.g.

self deny: DbRegistry isOnline
description: ('I cannot meaningfully test connecting because we are already connected to <1p>'
expandMacrosWith: DbRegistry dbIdentifier).

The description string can be constructed dynamically but remember, the string will be constructed whether or not the assertion fails; e.g. in the example above, we write <1p> instead of <1s> to avoid DNUing when dbIdentifier returns nil.

Resumable assertions
The default case is that a single failure halts a test, bypassing the remaining code and going immediately to #tearDown (or, if the failing assertion is in #tearDown, aborting the rest of that method): the assumption is that later code in a test depends on earlier code having passed. Sometimes this is not so and it is more useful to see when debugging (and/or to log when running) the results from several assertions in a test. Example:

#(‘same’ ‘*’ ‘*.txt’ ‘a*c’) with: #(‘same’ ‘any’ ‘some.txt’ ‘abc’) do:
[:eachMeta :eachString |
self assert: (eachMeta match: eachString)
description: (‘<1s> does not match <2s>’ expandMacrosWith: eachMeta with: eachString)
resumable: true].

Raising a ResumableTestFailure means that all the assertions will be run. If the test case is logging, this will print out a message to the log for each one that fails. When debugging, the user can hit 'proceed' to continue the test and see which other expressions do not match.

Test resources
A TestResource class holds a singleton instance that is set up when the first test that uses it is encountered and torn down at the end of the test suite's run. TestResource understands #assert:description:, #deny:description:, etc., so that code written in the #setUp and #tearDown of a TestCase can easily be refactored into the #setUp and #tearDown of a TestResource if it starts to take too long.

The order returned by TestCase class>>resources and TestResource class>>resources is respected: resources are set up in that order and torn down in the reverse order. For example, suppose a test requires both a resource that provides a database connection and another resource that adds some data temporarily to a database; it can provide the resources in order and rely on their setUps and tearDowns being called in the correct order. Where a resource is required by two other resources, it will not be torn down until both requirers have been torn down.

TestResource class>>availableFor: defines what a resource does when a test or another resource requires it. The standard behaviour is to assert that it is available, so raising a TestFailure if it is not, but a specialised resource subclass might override this.

TestCase and TestResource share a common superclass TestAsserter, which understands the #assert:... and #deny:... protocol (its class-side understands #assert:description:). If you find yourself refactoring common behaviour from test cases or test resources into a helper class, this may be a convenient superclass to use.

Logging support
The description strings described above may be logged to a Stream such as the Transcript, a file, stdout, etc.
- Choose whether to log by overriding TestAsserter class>>isLogging
- Choose where to log by overriding TestAsserter class>>failureLog or changing SUnitNameResolver defaultLogDevice (default is Transcript).