After some teasing, tugging and pulling I finally got my head around what OJSpec was up to. After understanding what was going on, I stripped it bare and integrated the essential parts of the framework into OJTest. OJSpec matchers are now available for use! I am actually really enjoying writing tests using it and I will probably use OJSpec matchers in my tests from now on (not something that I expected!).

OJSpec

Before we dive into how to use the new matchers in OJTest, we should talk a little history. OJSpec was around before I even started using Cappuccino and Antonio Cardozo was the creator. It was always a framework that I was interested in but never had the time to explore. When we started the OJTest repository, I asked Antonio if we could co-opt his work into the OJTest repository. He graciously agreed and we are appreciative of his gift!

Enable OJSpec

In order to enable OJSpec matchers, you will need to add a -s argument to your ojtest command:

ojtest Test/*.j

becomes

ojtest -s Test/*.j

This is, however, only necessary as long as OJSpec is an experimental feature. When it feels like OJSpec has been used enough to rely on it working, we will enable it by default.[1] Until then, though, you'll have to add the extra parameter.

The Matchers

There are currently 10 different matchers in the repository. These matchers are

    shouldEqual:
    shouldNotEqual:
    shouldBeNil:
    shouldNotBeNil:
    shouldBeSameAs:
    shouldNotBeSameAs:
    shouldBeInstanceOf:
    shouldNotBeInstanceOf:
    should:by:
    should:

Each of these matchers are attached to every Cappuccino object (including toll free bridged objects like strings and arrays). They are built on top of the OJAssert library currently used by OJUnit and the OJTest test runner will catch everything exactly the same way as it does for OJUnit. The difference is in the syntax.

The Syntax

The best way to explain why this different approach might be better or worse is to show a side by side comparison.

The OJUnit way

    [OJAssert assert:expected equals:actual];

The OJSpec way

    [actual shouldEqual:expected];

I really like the second syntax. This is also a bit of a cherry-picked scenario, though. Let's try something a little more complicated. Let's try to verify that something loops 5 times.

The OJUnit way

    var i = 0;
    while([anObject shouldLoop])
        i++
    [OJAssert assert:5 equals:i];

The OJSpec way

    [0 should:"be 5" by:function(i) {
        while([anObject shouldLoop])
            i++
        [i shouldEqual:5];
    }];

In this case, the OJSpec way is a bit more verbose but also a bit more description. It'll also allow us to give more detailed readouts (not yet, though!) on what is happening within our code. If this is a common case, we could also build this into the library.

Matchers Spec

Each matcher operates on an object. The parameters, however, vary. Below is a rough Doxygen documentation draft.

    /**
     * Verifies that one object is equal to the other using == or isEqual
     * @param anObject the object to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldEqual:(id)anObject
    
    /**
     * Verifies that one object is not equal to the other using != or !isEqual
     * @param anObject the object to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldNotEqual:(id)anObject
    
    /**
     * Verifies that an object is nil
     * @throws AssertionFailedError
     */
    CPObject-shouldBeNil
    
    /**
     * Verifies that an object is not nil
     * @throws AssertionFailedError
     */
    CPObject-shouldNotBeNil
    
    /**
     * Verifies that one object is the same object as another using ===
     * @param anObject the object to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldBeSameAs:(id)anObject
    
    /**
     * Verifies that one object is not the same object as another using ===
     * @param anObject the object to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldNotBeSameAs:(id)anObject
    
    /**
     * Verifies that one object is an instance of a class
     * @param aClass the class to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldBeInstanceOf:(Class)aClass
    
    /**
     * Verifies that one object is not an instance of a class
     * @param aClass the class to test against
     * @throws AssertionFailedError
     */
    CPObject-shouldNotBeInstanceOf:(Class)aClass

    /**
     * Runs a test with a bounded, named context
     * @param aDescription the description of the test
     * @param singleArgClosure aClosure with the object passed into it
     * @throws AssertionFailedError
     */
    CPObject-should:(CPString)aDescription by:(Function)singleArgClosure

    /**
     * Stubs a test and marks it as a failure
     * @param aDescription the description of the test
     * @throws AssertionFailedError
     */
    CPObject-should:(CPString)aDescription

Hopefully this is just a small step and we can get into doing things like a custom OJSpec test runner and all that fun jazz. In the meantime, start using this and let me know what you think! Thanks!

[1] It isn't enabled by default currently because OJSpec modifies the CPObject class which can be very dangerous and may have unknown consequences. In order to make sure that it doesn't effect you, you should first run your tests with OJSpec enabled. If you have poor test coverage or no tests, you can do a manual end-to-end test of your application by including @import <OJSpec/OJSpec.j> in your AppController.