NuBacon -- small RSpec clone
"Truth will sooner come out from error than from confusion." ---Francis Bacon
It is a Behavior-Driven Development test library for Nu and in extension for Objective-C. It is being developed while using in our iOS application, more on that will be announced.
Installation for command-line usage and OS X Xcode projects
There's currently no Nu specific package manager, so you will have to grab the source directly:
As a zip archive:
$ curl https://github.com/alloy/NuBacon/zipball/0.1 -o NuBacon-0.1.zip $ unzip NuBacon-0.1.zip
Or as a git clone:
$ git clone email@example.com:alloy/NuBacon.git $ cd NuBacon $ git checkout 0.1
Or checkout master if you’re feeling adventurous. The runloop code, for instance, is not yet available in a release.
Installation for iOS Xcode projects
- Follow the steps above to obtain the NuBacon source.
- Add NuBacon/iOSRunner/NuBacon-iOSRunner.xcodeproj to your project.
- Create a new application target, which will be the spec runner. Hereafter referred to as ‘the app’.
- In the General info window add NuBaconLib to ‘Direct Dependencies’.
- In the Build info window add to ‘Other Linker Flags’: -ObjC -all_load (Otherwise it will not be able to find the AppDelegate class.)
- Make the app link against liNuBaconLib.a
- Remove NSMainNibFile from the app’s Info.plist.
- Add the NuBacon sources to the runner app’s resources by opening NuBacon/iOSRunner/NuBacon-iOSRunner.xcodeproj and dragging the ‘NuBacon’ group to your project.
- Finally, add all the app's sources to the runner app (.h/.m) At least be sure that there’s something to compile, or the app won't run on the simulator. A SpecHelper.m file will always come in handy.
(load "bacon") (set emptyArray (do (array) (eq (array count) 0))) (describe "An array" `( (before (do () (set @ary (NSArray array)) (set @otherArray (`("noodles") array)) )) (it "is empty" (do () (~ @ary should not containObject:1) )) (it "has zero elements" (do () (~ @ary count should be:0) (~ @ary count should not be closeTo:0.1) ; default delta of 0.00001 (~ @ary count should be closeTo:0.1 delta:0.2) )) (it "raises when trying to fetch an element" (do () (set exception (-> (@ary objectAtIndex:0) should raise:"NSRangeException")) (~ (exception reason) should match:/beyond bounds/) )) (it "compares to another object" (do () (~ @ary should be:@ary) (~ @ary should equal:@ary) (~ @otherArray should not be:@ary) (~ @otherArray should not equal:@ary) )) (it "changes the count when adding objects" (do () (-> (@otherArray << "soup") should change:(do () (@otherArray count)) by:+1) )) (it "performs a long running operation" (do () (@otherArray performSelector:"addObject:" withObject:"soup" afterDelay:0.5) (wait 0.6 (do () (~ (@otherArray count) should be:2) )) )) ; Custom assertions are trivial to do, they are blocks returning ; a boolean value. The block is defined at the top. (it "uses a custom assertion to check if the array is empty" (do () (~ @ary should be a: emptyArray) (~ @otherArray should not be a: emptyArray) )) (it "has super powers" (do () ; flunks when it contains no assertions )) )) ((Bacon sharedInstance) run)
Now run it:
$ nush readme_spec.nu An array - is empty - has zero elements - raises when trying to fetch an element - compares to another object - changes the count when adding objects - performs a long running operation - uses a custom assertion to check if the array is empty - has super powers [FAILURE] An array - has super powers: flunked [FAILURE] 8 specifications (14 requirements), 1 failures, 0 errors
- should be:object
- should (be) a:object
- should equal:object
- should (be) closeTo:float | list of floats
- should (be) closeTo:float | list of floats delta:float
- should match:regexp
- should change:valueBlock
- should change:valueBlock by:delta
- should raise
- should raise:exceptionName
- should predicate method
- should dynamic predicate message matching
- should satisfy:message block:block
Any method of the object being tested, that can work as a predicate,
can be called on the BaconShould instance that wraps it. The result
of the method call will determine wether or not the assertion passes.
Any return value that evaluates to
true will pass, likewise any
value that evaluates to
false will fail. Unless the assertion has
been negated with
For instance, NSString has a
isAbsolutePath predicate method:
(~ "/an/absolute/path" should isAbsolutePath) (~ "a/relative/path" should not isAbsolutePath)
However, as you can see this does not always lead to proper English, therefor there are a few special rules on how these methods can be called.
If the predicate method starts with ‘is’ it can be omitted. The previous example can thus be rewritten as:
(~ "/an/absolute/path" should be an absolutePath) (~ "a/relative/path" should not be an absolutePath)
Method names in the third-person perspective can be called in the
first-person perspective. For example,
respondsToSelector: can be
called by omitting the ‘s’ from ‘responds’:
(~ "foo" should respondToSelector:"isAbsolutePath") (~ (NSArray array) should not respondToSelector:"isAbsolutePath")
after need to be defined before the first specification
that should have them applied.
You can nest contexts, which will run before/after filters of parent contexts like so:
(describe "parent context" `( (describe "child context" `( )) ))
You can define shared contexts in NuBacon like this:
(shared "an empty container" `( (it "has size zero" (do () (~ (@ary count) should be:0) )) (it "is empty" (do () (~ @ary should be: emptyArray) )) )) (describe "A new array" `( (before (do () (set @ary (NSArray array)) ) (behaves_like "an empty container") ))
These contexts are not executed on their own, but can be included with behaves_like in other contexts. You can use shared contexts to structure suites with many recurring specifications.
The ‘wait’ macro
Often in Objective-C apps, code will not execute immediately, but
scheduled on a runloop for later execution. Therefor a mechanism is
needed that will postpone execution of some assertions for a period of
time. This is where the
wait macro comes in:
(it "performs a long running operation" (do () ; Here a method call is scheduled to be performed ~0.5 seconds in the future (@otherArray performSelector:"addObject:" withObject:"soup" afterDelay:0.5) (wait 0.6 (do () ; This block is executed ~0.6 seconds in the future (~ (@otherArray count) should be:2) )) ))
The postponed block does not halt the thread, but is scheduled on the runloop as well. This means that your runloop based code will have a chance to perform its job before the assertions in the block are executed.
You can schedule as many blocks as you’d want and even nest them.
Nesting calls to assertions can become unreadable quite fast:
(((((@ary count) should) not) be) closeTo:0.1 delta:0.2)
For this purpose, the
~ macro has been introduced. It iterates over
the symbols in the given list and sends those as messages to the
object, which is the first item in the list:
(~ @ary count should not be closeTo:0.1 delta:0.2)
raise: assertions will execute the block, which is
the wrapped object, and assert that an exception is, or isn't, raised.
But creating a block and wrapping it in a BaconShould instance can
look a bit arcane, and you have to remember to use
((send (do () ((NSArray array) objectAtIndex:0)) should) raise:"NSRangeException")
-> macro has been introduced:
(-> (@ary objectAtIndex:0) should raise:"NSRangeException")
As you might have been able to tell, any extra messages are
dynamically dispatched by the
- Christian Neukirchen, and other contributors, for Bacon itself!
- Tim Burks for Nu
- Laurent Sansonetti for brainwashing me about lisps ;)
There's still plenty to do, see the TODO for things that need to be done.
Once you've made your great commits:
- Fork NuBacon
- Create a topic branch -
git checkout -b my_branch
- Push to your branch -
git push origin my_branch
- Create a pull request or issue with a link to your branch
- That's it!
Copyright (C) 2010 Eloy Durán firstname.lastname@example.org, Fingertips BV
NuBacon is freely distributable under the terms of an MIT-style license. See LICENSE or http://www.opensource.org/licenses/mit-license.php.