NuBacon

A small RSpec clone for the Nu and Objective-C programming languages

MIT License

Stars
10
Committers
2

NuBacon -- small RSpec clone

"Truth will sooner come out from error than from confusion."
                                           ---Francis Bacon

NuBacon is a Nu port of Bacon, a small Ruby RSpec clone.

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 protected]: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.

Whirl-wind tour

(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

Implemented assertions

  • should:predicateBlock
  • 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

Predicate methods

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

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

before/after

before and after need to be defined before the first specification that should have them applied.

Nested contexts

You can nest contexts, which will run before/after filters of parent contexts like so:

(describe "parent context" `(
  (describe "child context" `(
  ))
))

Shared contexts

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.

Helper macros

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)

The raise and 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:

((send (do () ((NSArray array) objectAtIndex:0)) should) raise:"NSRangeException")

Therefore the -> 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 ~ macro.

Thanks to

  • Christian Neukirchen, and other contributors, for Bacon itself!
  • Tim Burks for Nu
  • Laurent Sansonetti for brainwashing me about lisps ;)

Contributing

There's still plenty to do, see the TODO for things that need to be done.

Once you've made your great commits:

  1. Fork NuBacon
  2. Create a topic branch - git checkout -b my_branch
  3. Push to your branch - git push origin my_branch
  4. Create a pull request or issue with a link to your branch
  5. That's it!

LICENSE

Copyright (C) 2010 Eloy Durán [email protected], Fingertips BV <fngtps.com>

NuBacon is freely distributable under the terms of an MIT-style license. See LICENSE or http://www.opensource.org/licenses/mit-license.php.