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.
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:
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
See the Roadmap section for current and coming features, as well as some information about the Bumblebee project.
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
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.
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:
@RunWith is configured with the Bumblebee runner @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!
RubyCollectorThe RubyCollector used in the example allows you to:
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.
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.
AgileDoxCollectorThe 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.
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.
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.
This is the simplest comment possible
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.
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.
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.
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'}
*/
}
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.
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.
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.
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
}
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
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.
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
}
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'}
*/
}
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')}
*/
}
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";
}
}
Bumblebee tries to utilize Rubys excellent string manipulation functionality.
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).
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.
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
To enable simple, but powerful, formatting, a wiki dialect is used: The Muse wiki syntax
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)
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.
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]]
Headlines are accomplished with leading asterisks.
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 can also be used:
| column heading 1 | column heading 2 |
|---|---|
| 1.1 | 1.2 |
| 2.1 | 2.2 |
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}
<<<<
*/
}
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.
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)}
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.
JUnit3.x testcases can be used standalone or together with JUnit4 testcases.
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'}
*/
}
}
There are several ways in which to extend Bumblebee.
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.
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
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')}
*/
}
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 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.
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.
When a test fails there is additional information on the execution variable.
An example of a failing suite can be found here
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.
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'}
*/
}
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 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.
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);
}
}
This is a simple single argument theory. These are the data points submitted.
| string 1 | string 2 |
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 |
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.
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.
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
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.
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.
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.
Bumblebee is now JUnit 4.5beta compatible.
get_valueSimpler storage and retrieval methods for runtime data.
This is the first micro-release of Bumblebee.
com.agical.bumblebee.imaging.Imaging class for Swing and Web snapshots 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 featuresBumblebeeCollectors has moved, so classes using it needs to optimize imports in order to work. bootstrap.rb files are included in bumblebee, where bumblebee_bootstrap.rb is loaded first execution property on each node holding the execution meta information, and it has exception/completion hooks. Bugfixes and internal workThis 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 featuresBugfixes and internal workThe 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: