Let us setup to exemplify how to create a file with some contents using the FileSystem. We create a straightforward unary method:
<gtExample> converts any regular method into a GT-Example. The name of the method can be arbitrary.
In this example, we associated the example to the class side of the class that we want to exemplify. However, the location of a GT-Example may be freely chosen. But, let's not get ahead of ourselves.
What exactly is the effect of this pragma? The first visible impact is that several aspects of examples are directly accessible through the Nautilus, Spotter, and Inspector.
A GT-Example might be annotated with additional and optional meta-information like
#label: (a title or very short description),
#description: (a long description explaining the intention of the example) or
All supported annotations are documented in the Help-Browser with a short description.
The goal of an example is to exemplify a subject concept of interest. For example, that can be a class, a method or a package.
By default, each has an implicit subject, the class in which it is defined in. In the above example the implicit subject is a class-based subject referencing
FileSystem. Yet, we may declare additional subjects explicitly. For example, we could explicitly specify that the subject of our example is a method:
The example from above has the implicit subject
FileSystem and the additional explicit subject
Subjects implicitly form a meta-graph around examples that may be used to browse and navigate related examples. In the example above, browsing
FileReference using Nautilus (or Spotter and Inspector) will allow us to open related examples directly.
As examples return objects, we can use those objects to compose other objects that in their turn can also constitute examples. To support this, examples may be chained to form trees (or even graphs) of depending examples.
Confused? Let's look at our example. There are at least three interesting objects in one single method:
If we want to decompose them, we can use dependencies:
In the example above, the example [A] depends on the examples [B] and [C]. The order of declaration is important and maps to the arguments order declared by the method of example [A]. At runtime, examples that declare dependencies, receive the return values of their depending examples as arguments. As a consequence, the execution order goes bottom up.
Until now, we created dependencies by specifying the selector of a method in the same class. Yet, dependencies need not to be within the same class. To depend on examples built in other classes we have to use the appropriate pragma. For example, the example defined in
#classCommentContents does not really exemplify a FileSystem. Instead, it would be more appropriate to associate it with
External dependencies allow GTExamples to reuse existing examples from anywhere within the system while fully preserving the semantics of the link to the subjects.
All these dependencies sound nice, but let's take a pause for a moment. What happens at runtime if a dependency is declared more than once (within the graph of examples)?
Since examples may have side-effects, there is no suitable way of caching the return-value in order to reuse it. Therefore each example is performed every time it is used in a dependency. In other words, when the same example appears twice in a graph of dependencies, it will be executed twice and will produce two distinct return values that will have different identities.
Let's take an example: the static graph and the dynamic graph of the runtime of a GT-Example
#a:a: depending on the GT-Examples
#c:, each depending on GT-Example
In this situation, at runtime the example
#d is performed twice. The numbered squares visualize the order of execution.
Now, let's go deeper. What happens when the examples define circular dependencies?
The engine identifies this situation and it marks the example as invalid. An invalid example cannot be performed. This is an important feature.
In the following example, which contains a loop, the return-value of either example is nil. The corresponding result object will contain at least one dependency-exception.
One issue with our current example scenario is that the file created in
#createFile:onDisk: has the side effect of leaving that file reside on disk. In this situation we would like to cleanup such artifacts after running an example. We can do that with the declaration of a so called after-method.
The after-method can be any method, and it will receive as argument at most one argument given by the return value of the example method. In our case, this return value is the file reference.
When we perform the example from above, no file will be left over on the disk after the example finished running.
After-methods are similar to tearDown-methods in xUnit, however there are subtle and important differences:
A special case is depicted in the picture below, where you can see the example
#a:a depending on both
#b: and c: and each of these depending on
#d. In this situation, there will be two instances of
Until now, we considered positive examples. Yet, equally interesting is to document what should not be allowed. In other words, we may want to define negative examples. This can be achieved through explicit exception handling.
Multiple declarations of #raises: can be defined. In such a case at least one of the declared exceptions must be raised while the example is performed. If none of the defined exceptions is raised, the example is treated as a failing example (invalid).
There is no distinction made between examples not defining exceptions but raising exceptions and examples defining exceptions but not raising any exception - except for the exception being raised during runtime. Both are treated as failing examples (invalid).
We learned about positive and negative examples. However it is sometimes necessary to test the assumption.Welcome assertions:
Failing assertions are being treated as unexpected exceptions and will result in a failing example (invalid).
Stepping back, we can express everything that we can express with regular xUnit tests:
But, with examples, we can go beyond what is typically possible with xUnit. As we have only example as a concept, we can write assertions even in "setup" examples. Furthermore, we have a way to reuse code at multiple levels, not just two (i.e., setup/test). All in all, besides fulfilling the role of documentation, examples also offer an alternative for testing.
Examples may share parts of their declarations. This is useful whenever examples are implemented by a dedicated class or to reduce multiplication of annotations.
Right now, only subjects can be shared among examples. To this end, we can implement the
#gtExamplesSubjects method and make it return an array of possible subjects.
By default each class providing examples is added implicitly as a subject to its examples.
Whenever an example is performed, a context-object (key-value store) is created and is made available through a dynamic variable that can be called through #gtExampleContext. For example:
Using #gtExampleContext allows us to access the example under execution and to temporarily store information to the executing context. This context is also available from within the after-method. This is particularly useful when the side effects of an example are deep, and the after-method can have access to these side-effects and clean them up.
Each class providing examples understands the message
#gtExamples which when sent will return the examples of that class.
Some entities define a holder of examples like instances of
RBEnvironment and others. Their "contained" examples can be returned by sending the message
Since a method can only return one example,
CompiledMethod also understands both
#gtExample which will return the sole GTExample defined by it.
In some cases
#gtExamplesContained return the same result.
Other useful methods to handle examples:
Furthermore, Nautilus, the World-Menu, all Rub-Text-Editors as well as Spotter and Inspector provide access to retrieve, browse and navigate examples from entities within the world.
Besides the integration with Nautilus, Examples are also tightly integrated with Spotter and Inspector. The later for example will automatically visualize the graph of a GT-Example as well as the return-value. Integrations of GT-Examples for Spotter and Inspector are very extensible due to the moldable design of those tools.
Let's consider Roassal, which comes with an extensive set of examples. The Inspector allows you to browse all examples of Roassal visually. The Tab "Playground" (a custom extension by Roassal) lets you to "play" with the current example.
While playing with the return-values of examples is important, it can also be relevant to understand the static dependencies of examples. In our example regarding the FileSystem. To this end, the inspector shows
The currently selected example is highlighted with a grey color. The arrows show the relation of dependency. The shape and border of the example has no meaning.
Additional annotations can be implemented by providing the corresponding method in the class #GTExample as following:
The annotation can then be used as the following:
The method defining the example may be placed on the instance- or class-side. You may also consider to put all or a subset of examples into a dedicated class providing only examples.
Instance-side GT-Examples require to create plain instances of a class. Since this is not possible for all classes in the system, an explicit override of the factory has to be implemented. This is shown in the following code example for the class #Magnitude.
The override specifies the “provider” of examples, which in this case is the class #Magnitude itself. This override does not affect the class-side GT-Examples.
The following core classes implement such an override and therefore do not allow instance-side GT-Examples: SmalltalkImage, Context, Magnitude.
Examples come with a significant amount of dedicated terms and entities. Here is a glossary of these terms and their brief definition.