1. Examples

1.1. An example to start with

Let us setup to exemplify how to create a file with some contents using the FileSystem. We create a straightforward unary method:

FileSystem class >> #createFileOnDisk
     <gtExample>
     ^ FileSystem workingDirectory / 'test.txt'
          writeStreamDo: [ :stream | stream nextPutAll: self comment ];
          yourself

The Pragma <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 #icon:.

FileSystem class >> #createFileOnDisk
     <gtExample>
     <description: 'Create a new file or override an existing file with some contents. Open and close the stream safely'>
     ^ FileSystem workingDirectory / 'test.txt'
          writeStreamDo: [ :stream | stream nextPutAll: self comment ];
          yourself

All supported annotations are documented in the Help-Browser with a short description.

1.2. Exemplifying subjects

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:

FileSystem class >> #createFileOnDisk
     <gtExample>
     <description: ‘Create a new file or override an existing file with some contents. Open and close the stream safely’>
     <subjectClass: #FileReference selector: #writeStreamDo:>
     ^ FileSystem workingDirectory / 'test.txt'
          writeStreamDo: [ :stream | stream nextPutAll: self comment ];
          yourself

The example from above has the implicit subject FileSystem and the additional explicit subject FileReference>>#writeStreamDo:.

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.

1.3. Example dependencies

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:

  1. [A] the file reference
  2. [B] the string contents
  3. [C] the file reference with string contents

If we want to decompose them, we can use dependencies:

FileSystem class >> #createFile: aFileReference onDisk: aString  
     "[A]" 
     <gtExample>
     <depends: #sampleTextFileReference>
     <depends: #classCommentContents>
     <description: 'Create a new file or override an existing file with some contents. Open and close the stream safely'>
     <subjectClass: #FileReference selector: #writeStreamDo:>
     ^ aFileReference
          writeStreamDo: [ :stream | stream nextPutAll: aString ];
          yourself
FileSystem class >> #sampleTextFileReference
     "[B]"  
     <gtExample>
     ^ FileSystem workingDirectory / 'sample.txt'
FileSystem class >> #classCommentContents
     "[C]"    
     <gtExample>
     ^ self comment

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.

1.4. External dependencies

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 String:

FileSystem class >> #createFile: aFileReference onDisk: aString  
     "[A]" 
     <gtExample>
     <depends: #sampleTextFileReference>
     <dependsClass: 'String class' selector: #classCommentContents>
     <description: 'Create a new file or override an existing file with some contents. Open and close the stream safely'>
     <subjectClass: #FileReference selector: #writeStreamDo:>
     ^ aFileReference
          writeStreamDo: [ :stream | stream nextPutAll: aString ];
          yourself
FileSystem class >> #sampleTextFileReference
     "[B]"  
     <gtExample>
     ^ FileSystem workingDirectory / 'sample.txt'
String class >> #classCommentContents
     "[C]"    
     <gtExample>
     ^ self comment

External dependencies allow GTExamples to reuse existing examples from anywhere within the system while fully preserving the semantics of the link to the subjects.

1.5. Dealing with diamond dependencies

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 #b: and #c:, each depending on GT-Example #d.

In this situation, at runtime the example #d is performed twice. The numbered squares visualize the order of execution.

1.6. What about circular dependencies?

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.

1.7. Cleaning up after using an example

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.

FileSystem class >> #createFile: aFileReference onDisk: aString  
     "[A]" 
     <gtExample>
     <depends: #sampleTextFileReference>
     <dependsClass: 'String class' selector: #classCommentContents>
     <after: #deleteFileFromDisk:>
     <description: 'Create a new file or override an existing file with some contents. Open and close the stream safely'>
     <subjectClass: #FileReference selector: #writeStreamDo:>
     ^ aFileReference
          writeStreamDo: [ :stream | stream nextPutAll: aString ];
          yourself
FileSystem class >> #deleteFileFromDisk: aFileReference     
     ^ aFileReference delete

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:

  • The after-method is dedicated (but not exclusive) to a single example while tearDown (in most xUnits) is a global implementation covering multiple test-cases at once.
  • When multiple examples are chained, each declaring an after-method, multiple after-methods will be performed, in the same order as their corresponding examples.
  • Since each example intends to focus on a (fine-grained) object and/or behaviour, so does its after-method, only “tearing-down” the object/subject of its example

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 #d.

1.8. Exceptions

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.

Array class >> #addElement
     <gtExample>
     <raises: #ShouldNotImplement>
     ^ {} add: 1

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).

1.9. Assertions

We learned about positive and negative examples. However it is sometimes necessary to test the assumption.Welcome assertions:

FileSystem class >> #createFileOnDisk
     <gtExample>
     <after: #deleteFileFromDisk:>
     | newFile |
     newFile := FileSystem workingDirectory / 'test.txt'.
     self assert: newFile exists not.
     newFile writeStreamDo: [ :stream | ].
     self assert: newFile exists.
     ^ newFile
FileSystem class >> #deleteFileFromDisk: aFileReference 
     self assert: aFileReference exists.
     aFileReference delete.
     self assert: aFileReference exists not.
     ^ aFileReference

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:

  • setUp: We can define shared example objects by using dependencies.
  • assertions: We can write assertions in each example.
  • tearDown: We can define behavior to cleanup after the example execution (like tearDown).

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.

1.10. Shared Declarations

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.

ExamplesOfFileSystem >> #gtExamplesSubjects
     ^ { FileSystem. FileReference. FileStore }

By default each class providing examples is added implicitly as a subject to its examples.

TClassDescription >> #gtExamplesSubjects
     ^ { self }

1.11. Context

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:

GTExample class >> #thisExample
     <gtExample>
     ^ self gtExampleContext 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.

1.12. Accessing GTExamples - API

Each class providing examples understands the message #gtExamples which when sent will return the examples of that class.

Array gtExamples "all examples of the class #Array"

Some entities define a holder of examples like instances of SmalltalkImage, RPackage, RTag, RBEnvironment and others. Their "contained" examples can be returned by sending the message #gtExamplesContained.

Smalltalk gtExamplesContained “ all examples of the current smalltalk image “

Since a method can only return one example, CompiledMethod also understands both #gtExamplesContained and #gtExample which will return the sole GTExample defined by it.

(Bag class >> #gtExampleEmptyBag) gtExample

In some cases #gtExamples and #gtExamplesContained return the same result.

Array gtExamples = Array gtExamplesContained

Other useful methods to handle examples:

run "run the example and return its return-value"
debug "same run, but open debugger if the example fails"
subjects  "all subjects"
hasSubject: "check whether the given argument is a defined subject"
dependencies  "all directly depending examples"
directDependents  "all directly dependent examples"
subjects  "all subjects"
returnValue "the return-value"
result "the result object, containing possible exceptions, state and return-value"
hasLiteralThorough: "check whether any meta-info or the method itself refers to the given literal"

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.

1.13. More on UI

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.

1.14. Custom pragmas for custom tool integration

Additional annotations can be implemented by providing the corresponding method in the class #GTExample as following:

GTExample >> #myCustomAnnotation: aSymbol
     <gtExamplePragma>
     <description: 'a short description of this annotation and its behaviour'>
     ...

The annotation can then be used as the following:

FileSystem class >> #createFileOnDisk
     <gtExample>
     <myCustomAnnotation: #MyCustomSymbol>
     <description: 'Create a new file or override an existing file with some contents. Open and close the stream safely'>
     ^ FileSystem workingDirectory / 'test.txt'
          writeStreamDo: [ :stream | stream nextPutAll: self comment ];
          yourself

1.15. When to use a class-based or instance-side GT-Example?

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.

  • class-based (most common use-case):
    • model examples for an instance / behaviour of a class
    • provide singular examples
  • instance-based:
    • model examples for a particular behaviour (method) of a particular instance
    • bundle multiple examples into a dedicated class having the same or different goal
    • build more complex examples, requiring the state of the instance

1.16. Restrictions of the implementation

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.

Magnitude >> #gtExamplesFactory
     ^ super gtExamplesFactory
          provider: self;
          yourself

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.

1.17. Glossary

Examples come with a significant amount of dedicated terms and entities. Here is a glossary of these terms and their brief definition.

  • Example: an example is a tiny stub object representing a GT-Example. It holds the references to its dependencies, subjects and many other entities. The default factory ensures each example exists only once.
  • After-method: the after-method is a method that is performed right after the example. If more than one example is performed, all after-methods will be performed at the end. Examples and after-methods are performed in the same order.
  • Subject: the subject is the entitiy of interest, what the example is about or what it is supposed to demonstrate. An example may have many subjects, but each subject at most once.
  • Dependency: the dependency is itself an example. An example has exactly as many dependencies as the number of arguments of the method that defines the example.
  • Return-Value: the return-value of an example is the return-value of the method that defines the example.
  • Result: the result is a tiny object holding the static and dynamic state of an example including its return-value. The result also knows about exceptions, problems and failures.
  • Factory: the factory is an internal-only entity that retrieves and instanciates examples from the code-base, in particular from methods.
  • Organizer: the organizer is an internal-only entity, a cache that holds all instantiated examples. It is also responsible for example-announcements and it keeps a tiny set of the most recently computed results
  • Source: The source is an internal-only entity that refers to a non-meta class. By default the source is the class of the provider.
  • Provider: The provider is an internal-only entity that refers to a non-meta class instance. by default the provider is a plain instance of the source.
  • Example-Method: the example-method is an internal-only entity, a kind of proxy object referring to the specified method. It is possible that the method does not exist. All method-like entities like dependencies, after-method or subjects are in fact example-methods.