The Bumblebee Documentation

Index

Bumblebee 0.8 documentation

Bumblebee is a small run'n'report framework that builds on top of JUnit to create documentation by example. It is open source and Apache 2 licensed.

A quick example

Bumblebee takes code like this:

public class IntroductionAndSimpleUsage {
    @Test
    public void createTimeReporter() throws Exception {
        /*!m1
        To create a time reporter object, simply invoke the *default constructor*
        #{snip meth.from.m1.to.m2}
        */
        TimeReporter timeReporter = new TimeReporter();
        /*!m2*/
    }
}

and turns it into documentation like this:


Introduction and simple usage

Create time reporter

To create a time reporter object, simply invoke the default constructor

TimeReporter timeReporter = new TimeReporter();

This document, the Bumblebee documentation, is created from the acceptance test suite of Bumblebee. A Bumblebee document is re-generated every time the tests are run, hence the documentation will always reflect the latest code.

Bumblebee can also be used to document Web GUIs (preferably using Selenium) and Swing GUIs, please see examples posted at the Bumblebee Google Group

This release

See the Roadmap section for current and coming features, as well as some information about the Bumblebee project.

Getting Bumblebee

The latest version of Bumblebee is available at The Agical Bumblebee download area

There is a bumblebee-all jar including Bumblebee and all dependencies. The bumblebee-all-no-junit jar contains everything except the JUnit jar. The bumblebee-all zip file contains the condensed version of the entire Bumblebee source tree, including all the dependencies.

If you are a Maven2 user, add http://www.agical.com/maven2 to your repository list and the following dependency to your pom.xml

<dependency>
    <groupId>com.agical.bumblebee</groupId>
    <artifactId>bumblebee-all</artifactId>
    <version>0.8</version>
    <scope>test</scope>
</dependency>

The entire codebase is available as open source at Bumblebee Launchpad Bazaar DVCS

Getting support

There is a Bumblebee Google Group where you can post questions or see what others have had issues with, and how they have been resolved.

Setting up Bumblebee

Create a JUnit4 test suite like this:

package com.agical.bumblebee.acceptance.examples.simple;

import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

import com.agical.bumblebee.collector.BumblebeeCollectors;
import com.agical.bumblebee.junit4.BumbleBeeSuiteRunner;
import com.agical.bumblebee.ruby.RubyCollector;

@RunWith(BumbleBeeSuiteRunner.class)
@SuiteClasses({TheSimplestTestCase.class})
@BumblebeeCollectors({RubyCollector.class})
public class RubyCollectorTestSuite {
    /*!!
    #{configuration.target_file='target/site/simple-setup.html';''}
    */
}

In this example there are some things that are not as the standard JUnit4 test-suite setup:

  1. @RunWith is configured with the Bumblebee runner
  2. @BumblebeeCollectors has to be configured with the desired Bumblebee collectors (more on collectors below).

@SuiteClasses is configured as for any other suite.

Create the JUnit test case and run the suite. If everything is correctly setup you will get the documentation in: target/site/simple-setup.html. And you are up and running!

The RubyCollector

The RubyCollector used in the example allows you to:

  1. Add text to the document by adding comments to your code
  2. Include code snippets easily
  3. Format the text easily using the muse wiki-syntax
  4. Use serializable values from execution in your documentation
  5. Include whole or parts of texts from files

The default assumes that the sources are structured according to the Standard Directory Structure. If that doesn't suit your needs just make your own subclass and change the target and sources to fit your needs.

The default target for the documentation is target/site/documentation.html, but if you want to change that you can put the following line in the Bumblebee comment of your root suite:

    #{configuration.target_file='target/site/bumblebee_doc.html';''}

If you want to have a special stylesheet, add a line like this:

#{configuration.stylesheet='src/site/css/stylesheet.css'}

Otherwise, Bumblebee will provide one for you.

At the moment, stylesheets only work if you keep the target in the default folder. This will be addressed in a coming release.

The sub-suite

If you have a nested suite structure you must configure the sub suites a bit differently than the main suite; this is an example from the Bumblebee documentation:

package com.agical.bumblebee.acceptance.helpers.extension;

import org.junit.runner.RunWith;
import org.junit.runners.Suite.SuiteClasses;

import com.agical.bumblebee.junit4.BumbleBeeSubSuiteRunner;

@RunWith(BumbleBeeSubSuiteRunner.class)
@SuiteClasses(AddRubyExtensions.class)
public class ExtendingBumblebee {
    /*!!
    There are several ways in which to extend Bumblebee.
    #{assert.contains 'Extending bumblebee', 'De-camelcasing of suite class name'}
    */
}

Note that the @RunWith is configured with the BumbleBeeSubSuiteRunner

An easier-to-use replacement for the current runners, which are just extensions of the JUnit Suite runner, are on the way but not quite finished yet.

The AgileDoxCollector

The AgileDoxCollector is a simple Bumblebee implementation that just prints out the de-camelcased names of the suites, test classes and tests methods in an HTML list item hierarchy. If used as is, it produces the HTML-file target/site/agile-dox.html, and it can be configured to write the file anywhere on your file system by extending it and calling the superclass constructor with the desired output file. The AgileDoxCollector cannot retrieve any comments, in order to do so you must use the RubyCollector.

About this document

Since the purpose of the documentation is to demonstrate how to use the different features of Bumblebee, most sections will have a structure where the method that is the base for the sections is presented as code, and it is the same method that is actually rendering that section. This can be a bit awkward/meta/recursive, but you will get the hang of it.

Demonstrating method comments

One of the key points with Bumblebee is to keep the code close to the comments, and to let the comments become the documentation. With that in mind, the most natural place to keep the comments is of course in the methods. Comments can also reside on a test-class or suite-class level.

This section is a demonstration of basic usage of method comments together with the Ruby execution of the comments.

Probably the simplest comment

This is the simplest comment possible

The simplest comment explained

This is the code that generated the section above:

public void probablyTheSimplestComment() throws Exception {
    /*!
    This is the simplest comment possible
    */
}

It will simply output the comment text as it is, and the section header is the de-camelcased version of the method name.

Make assertions about the output

It is possible to make assertions about what the resulting document should contain. This is an example from the previous section:

public void theSimplestCommentExplained() throws Exception {
    /*!
    This is the code that generated the section above:
    >>>>
    #{clazz.probablyTheSimplestComment}
    <<<<
    It will simply output the comment text as it is, and the section header is the 
    de-camelcased version
    of the method name.
    #{assert.contains 'This is the simplest' +' comment possible', 'Can include comments'}
    */
}

Asserts will be present here and there in the documentation, since the documentation also serves as acceptance tests for Bumblebee.

To avoid having the assertion match its own assertion string (the underlying comments are present as part of the document) the '+' is added in the assertion string through-out the document. Assertions are made on the wiki text produced by the comments in the current method, not globally on the document (this has changed between the 0.3 and 0.4 releases).

To produce a bounded code box, write >>>> on a line by itself, followed by one or more lines with snippet text (or calls), followed by <<<< on a line by itself.

A note on mixing testing and documentation

Some people argue that testing and documenting are separate responsibilities. However, splitting them up would result in a lot of duplication, thus breaking the DRY (don't repeat yourself) principle.

The SRP (single responsibility principle) is often defined as a module should only have one reason to change, and by that definition sharing documentation and acceptance testing could be done in the same module. Since the the correlation is strong between the documented features and the tested acceptance features, and splitting them up would result in breaking the DRY principle, tests and documentation is in one place in Bumblebee.

Override default header

By default, Bumblebee will generate a section header by de-camelcasing the method name or the simple name of the class (when there is no method, e.g. for test suites). To override this for occasional headlines, just set the header.

For this section the title would have been 'Name that will be overridden' but instead we set it to 'Override default header'. This is how you override the method name:

public void nameThatWillBeOverridden() throws Exception {
    /*!
    By default, Bumblebee will generate a section header by de-camelcasing the 
    method name or the simple name of the 
    class (when there is no method, e.g. for test suites).
    To override this for occasional headlines, just set the header. 
    #{set_header 'Override default header'}
    
    For this section the title would have been 'Name that will be  overridden' 
    but instead we set it to 'Override default header'.
    This is how you override the method name:
    >>>>
    #{meth}
    <<<<
    #{assert.not_contains 'Name'+' that will be overridden', 'Can replace headlines'}
    */
}

Ignored method

Getting java snippets

The Java snippet extraction DSL

The DLS helps extracting data from various sources of information. "Out-of-the-box" it currently supports getting various parts of source code, but since it is written in Ruby it can be easily extended at any level to customize functionality. This section focuses on getting Java-code into your documentation.

Include the currently executing method

This is how to include the current method using the DSL:

public void includeTheCurrentlyExecutingMethod() throws Exception {
    /*!
    This is how to include the current method using the DSL:
    >>>>
    #{meth}
    <<<<
    
    =meth= represents the currently executing method.
    
    #{assert.contains 'public'+' void includeTheCurrentlyExecutingMethod', 
    'Can include current method'}
    */
}

meth represents the currently executing method.

Include other method from current class

It is possible to include other methods from the currently executing class by using the clazz object. This is what the output looks like:

public void demonstratingUsingMethodMissing() {
    // Demo method
}

...and this is how it is done:

public void includeOtherMethodFromCurrentClass() throws Exception {
    /*!
    It is possible to include other methods from the currently executing class 
    by using the =clazz= object. This is what the output looks like:
    >>>>
    #{clazz.demonstratingUsingMethodMissing}
    <<<<
    ...and this is how it is done:
    >>>>
    #{meth}
    <<<<
    This feature uses the *method_missing* feature in Ruby, i.e. when the 
    =clazz= object gets the call to =demonstratingUsingMethodMissing= 
    and no such method is found, it assumes that the call is intended to 
    return the method object of the specified =clazz= with the name 
    =demonstratingUsingMethodMissing= and returns it. 
    
    #{assert.contains 'public'+' void demonstratingUsingMethodMissing', 
    'Can include method from clazz object using method_missing'}
    */
}

This feature uses the method_missing feature in Ruby, i.e. when the clazz object gets the call to demonstratingUsingMethodMissing and no such method is found, it assumes that the call is intended to return the method object of the specified clazz with the name demonstratingUsingMethodMissing and returns it.

Avoid naming collision when including method

Sometimes there is a naming collision between the methods on the Ruby class and the ones in the Java class, and the above approach won't work. In those cases you can use the more explicit form clazz.meth 'methodName'

public void avoidNamingCollisionWhenIncludingMethod() throws Exception {
    /*!
    Sometimes there is a naming collision between the methods on the Ruby 
    class and the ones in the Java class, and the above approach won't work. 
    In those cases you can use the more explicit form =clazz.meth 'methodName'=:
    >>>>
    #{meth}
    <<<<
    The output will be similar to that of the previous example:
    >>>>
    #{clazz.meth('explicitlyGettingMethod')}
    <<<<
    #{assert.contains 'public'+' void explicitlyGettingMethod', 
    'Can include method from clazz with explicit method'}
    */
}

The output will be similar to that of the previous example:

public void explicitlyGettingMethod() {
    // Demo method
}

Using smaller code snippets

Consider the following code:

public void usingSmallerCodeSnippets() throws Exception {
    /*!m1
    Consider the following code:
    >>>>
    #{meth}
    <<<<
    =meth.from.m1.to.m2= retrives the code between the comments with the 
    markers (=m1= and =m2= in this case) and it looks like this:
    >>>>
    #{meth.from.m1.to.m2}
    <<<<
    Note that the last variable is not in this snippet.
    */
    String exampleCode = "Hello, I'm a code snippet!";
    /*!m2
    Note also that there can be several comments in one file, but the comments can be 
    empty (contain only the marker) to serve as markers
    #{assert.contains 'String'+' exampleCode', 'Can include smaller code snippets'}
    */
    boolean thisVariableWillNotBeInTheFirstSnippet = true; 
    /*!m3*/
}

meth.from.m1.to.m2 retrives the code between the comments with the markers (m1 and m2 in this case) and it looks like this:

String exampleCode = "Hello, I'm a code snippet!";

Note that the last variable is not in this snippet.

Note also that there can be several comments in one file, but the comments can be empty (contain only the marker) to serve as markers

Include entire source file

It is possible to include an entire file in the result:

package com.agical.bumblebee.acceptance.helpers;

import java.util.HashMap;

public class ShortClass {
    public void ps() {
        new HashMap();
    }
}

This is how it is done:

public void includeEntireSourceFile() throws Exception {
    /*!m1
    It is possible to include an entire file in the result:
    >>>>
    #{clazz('com.agical.bumblebee.acceptance.helpers.ShortClass')}
    <<<<
    This is how it is done:
    >>>>
    #{meth}
    <<<<
    Note that it is the same =clazz= method, and this time with a parameter telling which 
    class to represent. 
    #{assert.contains 'public'+' class ShortClass', 'Can include entire source file'}
    */
}

Note that it is the same clazz method, and this time with a parameter telling which class to represent.

Get code from method in other class

Now consider the following method:

public void getCodeFromMethodInOtherClass() throws Exception {
    /*!m1
    Now consider the following method:
    >>>>
    #{meth}
    <<<<
    The =clazz('class==_==name').methodName= method helps you retrieve methods 
    from other classes:
    >>>>
    #{clazz('com.agical.bumblebee.acceptance.helpers.MethodTester').testingMethod}
    <<<<
    #{assert.contains 'public'+' void testingMethod', 'Can include other methods'}
    */
}

The clazz('class_name').methodName method helps you retrieve methods from other classes:

public void testingMethod() {
    // Demo method
}

Include overloaded method

When the method to be included is available with several signatures you need to provide the signature to select what method to use, to produce something like this:

public void methodWithSignature(String s, int i) {
    // Demo method String, int
    System.out.println(s + i);
}

We use the current class here, but it can be done for any class by specifying the desired class as argument to the clazz method.

This is what the method that produced this section looks like:

public void includeOverloadedMethod() throws Exception {
    /*!m1
    When the method to be included is available with several signatures you need to provide 
    the signature to select what method to use, to produce something like this:
    >>>>
    #{clazz.methodWithSignature('java.lang.String', 'int')}
    <<<<
    We use the current class here, but it can be done for any class by specifying the desired 
    class as argument to the =clazz= method.
    
    This is what the method that produced this section looks like:
    >>>>
    #{meth}
    <<<<
    #{assert.contains 'Demo'+' method String, int', 
    'Can include methods with overloaded signatures'}
    */
}

You do not have to specify parameters if the method is not overloaded

If the method you'd like to include has arguments, but isn't overloaded, then you don't need to specify the arguments:

public void methodNotOverloadedButWithParameters(Integer i, String s) {
    // Method with arguments that is included without having to specify parameters
    System.out.println(s + i);
}

This is generally something that is nice since specifying all those parameters can be pretty tiring.

This is what the method looks like:

public void youDoNotHaveToSpecifyParametersIfTheMethodIsNotOverloaded() throws Exception {
    /*!m1
    If the method you'd like to include has arguments, but isn't overloaded, then you don't 
    need to specify the arguments:
    >>>>
    #{clazz.methodNotOverloadedButWithParameters}
    <<<<
    This is generally something that is nice since specifying all those parameters can be 
    pretty tiring.
    
    This is what the method looks like:
    >>>>
    #{meth}
    <<<<
    #{assert.contains('public'+' void methodNotOverloadedButWithParameters', 
    'Can include methods with signature without specifying its arguments')}
    */
}

Get code from inner class

Inner classes are difficult to parse (at least using QDox), and cannot be used in the same way as normal classes. What can be done is to simply extract the class using a regular expression and some comment markers:

public void getCodeFromInnerClass() throws Exception {
    /*!m1
    Inner classes are difficult to parse (at least using QDox), and cannot be used in the same way as 
    normal classes. What can be done is to simply extract the class using a regular 
    expression and some comment markers:
    >>>>
    #{meth}
    <<<<
    For the entire class:
    >>>>
    #{clazz.to_s.match(Regexp.new('//Start'+'tag(.*)//Endtag', Regexp::MULTILINE))[1]}
    <<<<
    #{assert.contains 'public class InnerClass', 'Can include inner classes'}
    */
}

For the entire class:

    public class InnerClass {
        public void someMethod() {
            String doing = "something";
        }
    }
    

Including slices and dices of text

Bumblebee tries to utilize Rubys excellent string manipulation functionality.

Include arbitrary file

This is how to include the contents of an arbitrary file into the documentation, here as a snippet:

This is a Bumblebee example file.
public void includeArbitraryFile() throws Exception {
    /*!m1
    This is how to include the contents of an arbitrary file 
    into the documentation, here as a snippet:
    >>>>
    #{File.new('src/test/resources/demofile.txt').read}
    <<<<
    
    >>>>
    #{meth}
    <<<<
    
    The filename can be relative to the execution directory 
    or an absolute path (not recommended).
    #{assert.contains('This'+' is a Bumblebee example file.', 
    'Can include arbitrary files')}
    */
}

The filename can be relative to the execution directory or an absolute path (not recommended).

Include line ranges

This is how you include certain lines:

public void includeLineRanges() throws Exception {
    /*!m1
    This is how you include certain lines:
    >>>>
    #{meth}
    <<<<
    The first argument to =lines= is the starting line (one-based), and 
    the second argument is how many lines 
    to include.
    
    This is what it looks like for a file:
    >>>>
    #{File.new('src/test/resources/multiline.txt').read.lines(1,3)}
    <<<<
    
    =lines= is a method added onto the Ruby =String= object, and can be used 
    on Strings through-out.
    
    #{assert.contains 'Line 1'+' of multiline.txt', 
    'Can include specific line range from content' }
    */
}

The first argument to lines is the starting line (one-based), and the second argument is how many lines to include.

This is what it looks like for a file:

Line 1 of multiline.txt
Line 2 of multiline.txt
Line 3 of multiline.txt

lines is a method added onto the Ruby String object, and can be used on Strings through-out.

Include text based on regular expression

Ruby has support for regular expressions, and they can be used as-is in Bumblebee:

public void includeTextBasedOnRegularExpression() throws Exception {
    /*!m1
    Ruby has support for regular expressions, and they can be used as-is in Bumblebee:
    >>>>
    #{meth}
    <<<<
    The output looks like this:
    >>>>
    #{File.new('src/test/resources/multiline.txt').read.match('Line 9(.*)')}
    <<<<
    
    #{assert.contains 'Line'+' 9 of multiline.txt', 
    'Can use regexp to include parts of content'}
    */
}

The output looks like this:

Line 9 of multiline.txt

Using wiki syntax

To enable simple, but powerful, formatting, a wiki dialect is used: The Muse wiki syntax

Basic inline formatting

The basic inline formattings available are emphasize, strong, fixed font and underlined. It is accomplished like this:

public void basicInlineFormatting() throws Exception {
    /*!
    The basic inline formattings available are *emphasize*,  **strong**, 
    =fixed font= and _underlined_. It is accomplished like this:
    >>>>
    #{meth}
    <<<<
    (for some strange reason the double-asterisk of strong requires two 
    leading spaces to not clog together with the preceeding
    emphasize)
    */
}

(for some strange reason the double-asterisk of strong requires two leading spaces to not clog together with the preceeding emphasize)

A note on indenting comments

Since wiki syntax is sensitive for indents and other subtle formattings, it is very important to indent your comments consistently. Each line in the comment will be stripped with the same indenting character sequence as the first line in the comment is indented with. I.e. do not leave the first line of a comment empty

If you are getting strange results, the reason is often that different lines are indented differently, e.g. one line is indented with spaces and another line with tabs etc.

Cross referencing within the document

Bumblebee will write anchor names for all suites, classes and methods in the document. This is what enables the index generation, and it can be used when you want to make cross references within the document. To do this, use a link tag like this:

[[#com.agical.bumblebee.acceptance.helpers.UsingWikiSyntax.basicInlineFormatting][Basic inline formatting]]

and it will render this link: Basic inline formatting

The common format is:

[[#classname.methodname][Link text]]

Headings

Headlines are accomplished with leading asterisks.

First level

Second level

Third level

Fourth level (lowest level)

It is accomplished like this:

public void headings() throws Exception {
    /*!
    Headlines are accomplished with leading  asterisks.
    
    * First level
    
    ** Second level
    
    *** Third level
    
    **** Fourth level (lowest level)
    
    It is accomplished like this:
    >>>>
    #{meth}
    <<<<
    Note the necessary space after the last asterisk and the necessary empty 
    line before each headline.
    
    These headings are parsed by the Wiki-syntax processor, and they may or 
    may not be part of the structural information of the document, depending 
    on how well the parser can be integrated with. So, until further notice, 
    consider these Wiki-headings as a presentational sugar rather than
    structural information.
    */
}

Note the necessary space after the last asterisk and the necessary empty line before each headline.

These headings are parsed by the Wiki-syntax processor, and they may or may not be part of the structural information of the document, depending on how well the parser can be integrated with. So, until further notice, consider these Wiki-headings as a presentational sugar rather than structural information.

Tables

Tables can also be used:

column heading 1 column heading 2
1.1 1.2
2.1 2.2
And the code looks like this:

public void tables() throws Exception {
    /*!
    Tables can also be used:
    column heading 1||column heading 2
    1.1|1.2
    2.1|2.2
    And the code looks like this:
    >>>>
    #{meth}
    <<<<
    */
}

Using runtime data

Passing runtime data to use in documentation

It is possible to pass runtime variables to use in the method comments. The data is inlined in the text when called. This is the an example where direct parameter access is used:

Result of toString method from MyObject

This method looks like this:

public void passingRuntimeDataToUseInDocumentation() throws Exception {
    /*!m1
    It is possible to pass runtime variables to use in the method comments. The data is 
    inlined in the text when called. This is the an example where direct parameter access is used: 
    
    *#{myKey2}*
    
    This method looks like this:
    >>>>
    #{meth}
    <<<<
    =store= is a static method on the class =Storage=, and it is a good candidate for a static import.
      
    #{assert.contains 'Result'+' of toString method from MyObject', 'Can include runtime variables'}
     */
    store("myKey2", new MyObject("The text"));
}

store is a static method on the class Storage, and it is a good candidate for a static import.

Rules and limitations for keys and values

The data stored must implement java.io.Serializable. This is largely a measure to enable storing data on disk if needed without breaking existing code.

To use direct access to the stored data, keys must conform to the Ruby symbol syntax, and also be accessed in such way.

If you would like to store data with other key strings, e.g. 'non-symbol key:' you can use the accessor method #{get_value(key)}

Calling methods on the runtime objects from the comments

The nice thing with JRuby is that you can call the method on the Java objects you store from within your Ruby code. This method looks like this:

public void callingMethodsOnTheRuntimeObjectsFromTheComments() throws Exception {
    /*!m1
    The nice thing with JRuby is that you can call the method on the 
    Java objects you store from within your Ruby code. This method looks like this:
    >>>>
    #{meth}
    <<<<
    and we can get the =getContainedData()= from the =MyObject=:  *#{myKey2.getContainedData()}*
    
    MyObject looks like this:
    >>>>
    #{clazz('com.agical.bumblebee.acceptance.helpers.MyObject')}
    <<<<
    
    JRuby has the feature that it converts Java-style method names to Ruby-style, and in this case we
    could have called 
    
    =contained_data= 
    
    just as well as 
    
    =getContainedData()= 
    
    However, since it 
    can be rather confusing mixing Ruby and Java code, it is recommended to call included 
    Java objects by their Java names.
    
    #{assert.contains 'The'+' text', 'Can call methods on runtime variables'}
     */
   store("myKey2", new MyObject("The" + " text"));
}

and we can get the getContainedData() from the MyObject: The text

MyObject looks like this:

package com.agical.bumblebee.acceptance.helpers;

import java.io.Serializable;

public class MyObject implements Serializable {
    private static final long serialVersionUID = -9047583716063916445L;
    private String string;

    public MyObject(String string) {
        this.string = string;
    }

    public String getContainedData() {
        return string;
    }
    
    public String toString() {
        return "Result of toString method from MyObject";
    }
}

JRuby has the feature that it converts Java-style method names to Ruby-style, and in this case we could have called

contained_data

just as well as

getContainedData()

However, since it can be rather confusing mixing Ruby and Java code, it is recommended to call included Java objects by their Java names.

Using JUnit3

JUnit3.x testcases can be used standalone or together with JUnit4 testcases.

So far, no limitations in that use

Thanks to JUnit4's handling of JUnit3.x test cases they work seamlessly with the Suites, hence allowing you to recycle you old test cases.

package com.agical.bumblebee.acceptance.helpers;

import junit.framework.TestCase;

public class JUnit3TestCase extends TestCase {
    /*!!
    #{set_header 'Using JUnit3'}
    JUnit3.x testcases can be used standalone or together with JUnit4 testcases.
    */
    public void testIfJUnit3AlsoWorks() throws Exception {
        /*!m1
        #{set_header 'So far, no limitations in that use'}
        Thanks to JUnit4's handling of JUnit3.x test cases they work seamlessly
        with the Suites, hence allowing you 
        to recycle you old test cases.
        >>>>
        #{clazz}
        <<<<
        #{assert.contains 'So far,'+' no limitations in that use', 'JUnit3 is included'}
        */
    }
    
}

Extending bumblebee

There are several ways in which to extend Bumblebee.

Add ruby extensions

Execute ruby code directly in comment

Now consider the following method:

public void executeRubyCodeDirectlyInComment() throws Exception {
    /*!m1
    Now consider the following method:
    >>>>
    #{meth}
    <<<<
    
    The comments are string templates that get executed in Ruby. Therefore 
    most Ruby string features can be used within the comment. In this case 
    we output the numbers 0 through 9.
    
    *#{s = "";10.times {|i| s+=i.to_s};s;}*
    
    This is often rather awkward, and one of the extension suggestions below will 
    probably be a better option when you want to execute more than the Ruby 
    code that comes with Bumblebee.
    #{assert.contains '0123456789', 'Outputs results of Ruby evaluation'}
    */
}

The comments are string templates that get executed in Ruby. Therefore most Ruby string features can be used within the comment. In this case we output the numbers 0 through 9.

0123456789

This is often rather awkward, and one of the extension suggestions below will probably be a better option when you want to execute more than the Ruby code that comes with Bumblebee.

Add a script in the comment using require

To include a script, just use the Ruby require statement.

public void addAScriptInTheCommentUsingRequire() throws Exception {
    /*!m1
    #{require('com/agical/bumblebee/acceptance/helpers/extension');''}
    
    To include a script, just use the Ruby =require= statement.
    >>>>
    #{meth}
    <<<<
    Since the require method returns =true= we have to prevent that from making it to the output
    by adding a =;''= 
    
    The file is named =extension.rb= and looks like this:
    >>>>
    #{File.new('src/test/resources/com/agical/bumblebee/acceptance/helpers/extension.rb').read}
    <<<<
    
    The method of the extension script can be invoked like any other method, 
    in this case the output is: =#{extension_method}=
    
    #{assert.contains 'result of extension method', 'Can use extension methods'}
    */
}

Since the require method returns true we have to prevent that from making it to the output by adding a ;''

The file is named extension.rb and looks like this:

  def extension_method
    'result of extension method'
  end

The method of the extension script can be invoked like any other method, in this case the output is: result of extension method

Execute a script from a comment and include result

Now we want to run the following Ruby script (i.e. program) and include the result in the documentation:

"The result of script evaluation: " + (4712-1).to_s

We just invoke Ruby's instance_eval on the contents of a from a file and get the result:

The result of script evaluation: 4711

This method looks like this:

public void executeAScriptFromACommentAndIncludeResult() throws Exception {
    /*!m1
    Now we want to run the following Ruby script (i.e. program) 
    and include the result in the documentation:
    >>>>
    #{File.new('src/test/resources/com/agical/bumblebee/acceptance/helpers/evaluate.rb').read}
    <<<<
    We just invoke Ruby's =instance_eval= on the contents of a from a file and get the result:
    
    #{instance_eval(File.new('src/test/resources/com/agical/bumblebee/acceptance/helpers/evaluate.rb').read)}
    
    This method looks like this: 
    >>>>
    #{meth}
    <<<<
    
    #{assert.contains( 'The result of script evaluation: 4711', 
    'Can evaluate scripts and include results')}
    */
}

Experimental

This section contains features that are of experimental status. They may or may not be there in the next release, and they may or may not change API and functionality. That said, even the other features of Bumblebee may change, but these are almost guaranteed to.

Invocation information

Invocation information is information regarding the method execution time and what method was called, and whether it returned normally or with an exception.

For the JUnit case this is sufficient, but if you would like to use Bumblebee in a more generic situation, there are some more data for those cases.

Get execution meta information

This example shows how to retrieve different kinds of meta information around the execution of a certain method.

Member time[ms]
getExecutionMetaInformation 21
InvocationInformation 25
Experimental 80
BumblebeeDocumentation 224

How it is done is shown here:

public void getExecutionMetaInformation() throws Exception {
    /*!
    #{current_execution=execution.getMethod();''}
    #{parent_class=parent.execution.getExecutingClass();''}
    #{grand_parent_class=parent.parent.execution.getExecutingClass();''}
    #{great_grand_parent_class=parent.parent.parent.execution.getExecutingClass();''}
    
    This example shows how to retrieve different kinds of meta information
    around the execution of a certain method.
    
    Member || time[ms]
    #{execution.getMethod().getName()} | #{execution.getExecutionTime()}
    #{parent_class.getSimpleName()} | #{parent.execution.getExecutionTime()}
    #{grand_parent_class.getSimpleName()}| #{parent.parent.execution.getExecutionTime()}
    #{great_grand_parent_class.getSimpleName()}| #{parent.parent.parent.execution.getExecutionTime()}
    
    How it is done is shown here:
    >>>>
    #{meth}
    <<<<
    Here we make use of a new feature, to add arbitrary parameters to a 
    node (=current_execution= etc). 
    
    The =execution= variable is holding the current execution information. 
    On a class/suite there is the java =Class= and the local execution time. 
    For the methods there is the java =Method= object and the execution time, 
    as shown above.
    
    */
    Thread.sleep(20);
}

Here we make use of a new feature, to add arbitrary parameters to a node (current_execution etc).

The execution variable is holding the current execution information. On a class/suite there is the java Class and the local execution time. For the methods there is the java Method object and the execution time, as shown above.

Getting failure information

When a test fails there is additional information on the execution variable.

An example of a failing suite can be found here

Bootstrapping

Sometimes it is necessary to bootstrap the entire Bumblebee execution. On those occasions it is not possible to use require in the comment (since the Ruby execution has already begun!). Then you need to add your own bootstrap.rb script. If you place it in the root of the classpath Bumblebee will add it automatically.

Simple bootstrapping

In our case the bootstrap.rb looks like this:

def verify_bootstrapping
    'Return value from extra bootstrap '+'method'
end

def same_m
    "local bootstrap"
end


This is the result of the bootstrapping method:

Return value from extra bootstrap method

And this is how it is done:

public void simpleBootstrapping() throws Exception {
    /*!
    In our case the =bootstrap.rb= looks like this:
    >>>>
    #{File.new('src/main/resources/bootstrap.rb').read}
    <<<<
     
    This is the result of the bootstrapping method: 
    >>>>
    #{verify_bootstrapping}
    <<<<
    
    And this is how it is done:
    >>>>
    #{meth}
    <<<<
    #{assert.contains 'Return'+' value from extra bootstrap method', 'bootstrap.rb is loaded and can be used'}
    */ 
}

Modifying traversers

This is an example of a suite that instead of having wiki text in the comments, it uses html directly. The result can be viewed
here

Parameterized tests

Parameterized tests can also be used. JUnit will execute the methods once for each parameter, and Bumblebee will get the corresponding calls. A good recommendation for large series of tests is probably to have the header to contain the parameter:

public void oneParameterizedTest() throws Exception {
    /*!
    #{set_header(data + ' First test')}
    */
    store("data", data);
}

As we see, you can store the actual data used in the test to display it in the report.

data1 First test

data1 Second test

data2 First test

data2 Second test

Using theories

It is also possible to use JUnit theories, an experimental feature in JUnit 4.4. In difference from using the Parameterized runner, each test will only be reported by JUnit once, so you need to store the data from each run in the test:

package com.agical.bumblebee.acceptance.helpers.experimental;

import static com.agical.bumblebee.junit4.Storage.store;
import static org.junit.Assume.assumeThat;

import java.util.ArrayList;

import org.hamcrest.core.IsEqual;
import org.junit.experimental.theories.DataPoint;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

import com.agical.bumblebee.junit4.BumbleBeeSuiteRunner;

@RunWith(Theories.class)
public class UsingTheories {
    /*!!
    It is also possible to use JUnit theories, an experimental feature in JUnit 4.4. 
    In difference from using the Parameterized runner, each test will only be reported 
    by JUnit once, so you need to store the data from each run in the test:
    >>>>
    #{clazz}
    <<<<
    */
    @DataPoint public static final String aString = "string 1";
    @DataPoint public static final String anotherString = "string 2";
    
    private static ArrayList<String> data = new ArrayList<String>();
    private static ArrayList<String> multiData = new ArrayList<String>();
    
    @Theory
    public void aTheory(String theStringToTest) throws Exception {
        /*!
        This is a simple single argument theory. These are the data points submitted.
        #{theory_data.collect {|datapoint| datapoint + ' | '}}
        */
         data.add(theStringToTest);
         store("theory_data", data);
    }
    
    @Theory
    public void anAssumingMultiArgumentTheory(String first, String second) throws Exception {
        assumeThat(first, new IsEqual<String>(aString));
        /*!
        You can use =assumeThat(...)= to filter which tests to run to the end, 
        and hence what data is stored:
        >>>>
        #{meth}
        <<<<
        In this case we use *assumeThat* to let the test continue for data sets 
        where the first argument is =\"string 1\"=:
        #{theory_data.collect {|datapoint| datapoint + ' | '}}
        */
        multiData.add(first + "-" + second);
        store("theory_data", multiData);
    }
    
}

A theory

This is a simple single argument theory. These are the data points submitted.

string 1 string 2

An assuming multi argument theory

You can use assumeThat(...) to filter which tests to run to the end, and hence what data is stored:

public void anAssumingMultiArgumentTheory(String first, String second) throws Exception {
    assumeThat(first, new IsEqual<String>(aString));
    /*!
    You can use =assumeThat(...)= to filter which tests to run to the end, 
    and hence what data is stored:
    >>>>
    #{meth}
    <<<<
    In this case we use *assumeThat* to let the test continue for data sets 
    where the first argument is =\"string 1\"=:
    #{theory_data.collect {|datapoint| datapoint + ' | '}}
    */
    multiData.add(first + "-" + second);
    store("theory_data", multiData);
}

In this case we use assumeThat to let the test continue for data sets where the first argument is "string 1"

string 1-string 1 string 1-string 2

Excluding content

Exclude test from report

This is how to exclude a test from the resulting document:

public void thisIsTheExcludedTest() throws Exception {
    /*!
    #{exclude}
    This comment won't be in the resulting document.
    
    #{configuration.traversers.wiki2doc.assert(self, 'Test can be excluded from the resulting document') {|doc|
        !doc.include?('This'+' is the excluded test')
    }}
    */
    
}

You need to exclude it from both the wiki2doc and the htmlindex traverser in order to make it disappear totally from the document.

This is also an example of how to make asserts on the resulting document. The result in the JUnit report is not very nice at the moment, but at least there is an error.

Roadmap

This section contains the history and the backlog of Bumblebee.

Bumblebee is starting to shake in place, and the documented features can be considered rather stable. User input will have a big say in the future development, and feedback from usage of Bumblebee is much appreciated in order to make it a better product. I will do my best to add any features requested to Bumblebee, or help you accomplish what you want in some other way.

You can email me at daniel.brolund@agical.com or, preferrably, join the Bumblebee group at Google Groups and start a discussion there.

Background

Features and characteristics

Bumblebee is especially suited for creating documents where code snippets or runtime data are necessary. Some of the features:

The general idea is to retrieve as much human-friendly documentation out of your test-cases as possible, with

  1. minimal effort
  2. minimal intrusion
  3. maximal expressive power
  4. maximal executional power
  5. maximal flexibility
  6. maximal extensibility

A brief look under the hood

Bumblebee gets events from the execution of the test suites/cases/methods and then parses the classes and methods for specific comments to include.

By default it adds the de-camelcased names of the suites/cases/methods as headings (a la AgileDox), and the contained comments becomes the paragraphs.

The comments are handled as Ruby-strings that get executed in a Bumblebee execution context before being wiki-transformed and added to the output. The execution context is the base for the DSL for including code-snippets and other things needed, and one of the core ideas of Bumblebee is that you should be able to extend that functionality easily.

When refactoring, the documentation will change with the code, since the snippets are always based on the last executed code! You will never get away from writing or changing the texts, but Bumblebee removes the drudgery of having to change it in several places.

The history of Bumblebee

Bumblebee springs from the experiences from TDDoc, a documentation add-on to RMock. The ideas and concepts are pretty much the same, but the implementation approach is different.

Projects using Bumblebee

Need more support?

Bumblebee is an open source project that I work on when I'm not consulting or otherwise tending my company Agical. If you desperately need some feature, please contact me at daniel.brolund@agical.com and we'll work something out.

Releases

Release 0.8 - JUnit4.5beta compatibility - 2008-07-01

Bumblebee is now JUnit 4.5beta compatible.

Release 0.7 - Simplified runtime data inclusion - 2008-06-27

Simpler storage and retrieval methods for runtime data.

Release 0.6 - Web and Swing documentation enablers - 2008-06-25

This is the first micro-release of Bumblebee.

Release 0.5 - Better feedback - 2008-06-21

This release has focused on getting better error messages and feedback from Bumeblebee to the user.

Packaging has improved, and now includes a bumblebee-all jar and a bumblebee-all-no-junit jar. For some reason the jars are not compressed by Buildr, which results in rather large download size. I'm sorry for any inconvenience.

This is the last aggregated release. Future releases will be made whenever a feature is ready, and hopefully that will result in a more steady flow, and this will eliminate the need for SNAPSHOTS releases.

New and changed features

Bugfixes and internal work

Release 0.4 - Get the Node model in place - 2008-02-26

This release had the goal of creating and exposing a more consistent node model. The release should only require minor changes in existing applications using Bumblebee, but the possibilities to retrieve data, to configure and style the documentation has improved significantly.

These improvements are partially explained in the documentation where needed, but the implementation has mostly been a refactoring of existing functionality, hence all new possibilities haven't been explored enough yet.

One of the important-to-remember changes is that class- and suite level comments are now commented with two !! instead of one ! to simplify parsing.

Below is a list of changes, improvements and bug-fixes.

New and changed features

Bugfixes and internal work

Release 0.3 - Multiple input formats - 2007-12-01

Release 0.2 - Object-orient the information extraction model - 2007-11-11

Release 0.1 - Get it out - 2007-11-01

Backlog

The sprints will be abandoned from after version 0.5, and new releases will be published as soon as any minimum marketable feature is implemented. This is the queue of possible items, and it will be affected by any user input given:

Queue

© 2007-2008 Agical AB