Use Nimble to express the expected outcomes of Swift or Objective-C expressions. Inspired by Cedar.
// Swift
expect(1 + 1).to(equal(2))
expect(1.2).to(beCloseTo(1.1, within: 0.1))
expect(3) > 2
expect("seahorse").to(contain("sea"))
expect(["Atlantic", "Pacific"]).toNot(contain("Mississippi"))
expect(ocean.isClean).toEventually(beTruthy())
- Some Background: Expressing Outcomes Using Assertions in XCTest
- Nimble: Expectations Using
expect(...).to
- Built-in Matcher Functions
- Writing Your Own Matchers
- Installing Nimble
Apple's Xcode includes the XCTest framework, which provides
assertion macros to test whether code behaves properly.
For example, to assert that 1 + 1 = 2
, XCTest has you write:
// Swift
XCTAssertEqual(1 + 1, 2, "expected one plus one to equal two")
Or, in Objective-C:
// Objective-C
XCTAssertEqual(1 + 1, 2, @"expected one plus one to equal two");
XCTest assertions have a couple of drawbacks:
- Not enough macros. There's no easy way to assert that a string contains a particular substring, or that a number is less than or equal to another.
- It's hard to write asynchronous tests. XCTest forces you to write a lot of boilerplate code.
Nimble addresses these concerns.
Nimble allows you to express expectations using a natural, easily understood language:
// Swift
import Nimble
expect(seagull.squawk).to(equal("Squee!"))
// Objective-C
#import <Nimble/Nimble.h>
expect(seagull.squawk).to(equal(@"Squee!"));
The
expect
function autocompletes to includefile:
andline:
, but these parameters are optional. Use the default values to have Xcode highlight the correct line when an expectation is not met.
To perform the opposite expectation--to assert something is not
equal--use toNot
or notTo
:
// Swift
import Nimble
expect(seagull.squawk).toNot(equal("Oh, hello there!"))
expect(seagull.squawk).notTo(equal("Oh, hello there!"))
// Objective-C
#import <Nimble/Nimble.h>
expect(seagull.squawk).toNot(equal(@"Oh, hello there!"));
expect(seagull.squawk).notTo(equal(@"Oh, hello there!"));
Nimble makes sure you don't compare two types that don't match:
// Swift
// Does not compile:
expect(1 + 1).to(equal("Squee!"))
Nimble uses generics--only available in Swift--to ensure type correctness. That means type checking is not available when using Nimble in Objective-C. 😭
Tired of so much typing? With Nimble, you can use overloaded operators
like ==
for equivalence, or >
for comparisons:
// Swift
// Passes if squawk does not equal "Hi!":
expect(seagull.squawk) != "Hi!"
// Passes if 10 is greater than 2:
expect(10) > 2
Operator overloads are only available in Swift, so you won't be able to use this syntax in Objective-C. 💔
The expect
function doesn't evalaute the value it's given until it's
time to match. So Nimble can test whether an expression raises an
exception once evaluated:
// Swift
// Note: Swift currently doesn't have exceptions.
// Only Objective-C code can raise exceptions
// that Nimble will catch.
let exception = NSException(
name: NSInternalInconsistencyException,
reason: "Not enough fish in the sea.",
userInfo: ["something": "is fishy"])
expect { exception.raise() }.to(raiseException())
// Also, you can customize raiseException to be more specific
expect { exception.raise() }.to(raiseException(named: NSInternalInconsistencyException))
expect { exception.raise() }.to(raiseException(
named: NSInternalInconsistencyException,
reason: "Not enough fish in the sea"))
expect { exception.raise() }.to(raiseException(
named: NSInternalInconsistencyException,
reason: "Not enough fish in the sea",
userInfo: ["something": "is fishy"]))
Objective-C works the same way, but you must use the expectAction
macro when making an expectation on an expression that has no return
value:
// Objective-C
NSException *exception = [NSException exceptionWithName:NSInternalInconsistencyException
reason:@"Not enough fish in the sea."
userInfo:nil];
expectAction([exception raise]).to(raiseException());
// Use the property-block syntax to be more specific.
expectAction([exception raise]).to(raiseException().named(NSInternalInconsistencyException));
expectAction([exception raise]).to(raiseException().
named(NSInternalInconsistencyException).
reason("Not enough fish in the sea"));
expectAction([exception raise]).to(raiseException().
named(NSInternalInconsistencyException).
reason("Not enough fish in the sea").
userInfo(@{@"something": @"is fishy"}));
Some testing frameworks make it hard to test primitive C values. In Nimble, it just works:
// Swift
let actual: CInt = 1
let expectedValue: CInt = 1
expect(actual).to(equal(expectedValue))
In fact, Nimble uses type inference, so you can write the above without explicitly specifying both types:
// Swift
expect(1 as CInt).to(equal(1))
In Objective-C, Nimble only supports Objective-C objects. To make expectations on primitive C values, wrap then in an object literal:
expect(@(1 + 1)).to(equal(@2));
In Nimble, it's easy to make expectations on values that are updated
asynchronously. Just use toEventually
or toEventuallyNot
:
// Swift
dispatch_async(dispatch_get_main_queue()) {
ocean.add("dolphins")
ocean.add("whales")
}
expect(ocean).toEventually(contain("dolphins", "whales"))
// Objective-C
dispatch_async(dispatch_get_main_queue(), ^{
[ocean add:@"dolphins"];
[ocean add:@"whales"];
});
expect(ocean).toEventually(contain(@"dolphins", @"whales"));
In the above example, ocean
is constantly re-evaluated. If it ever
contains dolphins and whales, the expectation passes. If ocean
still
doesn't contain them, even after being continuously re-evaluated for one
whole second, the expectation fails.
Sometimes it takes more than a second for a value to update. In those
cases, use the timeout
parameter:
// Swift
// Waits three seconds for ocean to contain "starfish":
expect(ocean).toEventually(contain("starfish"), timeout: 3)
// Objective-C
// Waits three seconds for ocean to contain "starfish":
expect(ocean).withTimeout(3).toEventually(contain(@"starfish"));
You can also provide a callback by using the waitUntil
function:
// Swift
waitUntil { done in
// do some stuff that takes a while...
NSThread.sleepForTimeInterval(0.5)
done()
}
// Objective-C
waitUntil(^(void (^done)(void)){
// do some stuff that takes a while...
[NSThread sleepForTimeInterval:0.5];
done();
});
waitUntil
also optionally takes a timeout parameter:
// Swift
waitUntil(timeout: 10) { done in
// do some stuff that takes a while...
NSThread.sleepForTimeInterval(1)
done()
}
// Objective-C
waitUntilTimeout(10, ^(void (^done)(void)){
// do some stuff that takes a while...
[NSThread sleepForTimeInterval:1];
done();
});
Nimble has full support for Objective-C. However, there are two things to keep in mind when using Nimble in Objective-C:
-
All parameters passed to the
expect
function, as well as matcher functions likeequal
, must be Objective-C objects:// Objective-C #import <Nimble/Nimble.h> expect(@(1 + 1)).to(equal(@2)); expect(@"Hello world").to(contain(@"world"));
-
To make an expectation on an expression that does not return a value, such as
-[NSException raise]
, useexpectAction
instead ofexpect
:// Objective-C expectAction([exception raise]).to(raiseException());
Nimble provides a shorthand for expressing expectations using the
expect
function. To disable this shorthand in Objective-C, define the
NIMBLE_DISABLE_SHORT_SYNTAX
macro somewhere in your code before
importing Nimble:
#define NIMBLE_DISABLE_SHORT_SYNTAX 1
#import <Nimble/Nimble.h>
NMB_expect(^{ return seagull.squawk; }, __FILE__, __LINE__).to(NMB_equal(@"Squee!"));
Disabling the shorthand is useful if you're testing functions with names that conflict with Nimble functions, such as
expect
orequal
. If that's not the case, there's no point in disabling the shorthand.
Nimble includes a wide variety of matcher functions.
// Swift
// Passes if actual is equivalent to expected:
expect(actual).to(equal(expected))
expect(actual) == expected
// Passes if actual is not equivalent to expected:
expect(actual).toNot(equal(expected))
expect(actual) != expected
// Objective-C
// Passes if actual is equivalent to expected:
expect(actual).to(equal(expected))
// Passes if actual is not equivalent to expected:
expect(actual).toNot(equal(expected))
Values must be Equatable
, Comparable
, or subclasses of NSObject
.
equal
will always fail when used to compare one or more nil
values.
// Swift
// Passes if actual has the same pointer address as expected:
expect(actual).to(beIdenticalTo(expected))
expect(actual) === expected
// Passes if actual does not have the same pointer address as expected:
expect(actual).toNot(beIdenticalTo(expected))
expect(actual) !== expected
// Objective-C
// Passes if actual has the same pointer address as expected:
expect(actual).to(beIdenticalTo(expected));
// Passes if actual does not have the same pointer address as expected:
expect(actual).toNot(beIdenticalTo(expected));
beIdenticalTo
only supports Objective-C objects: subclasses ofNSObject
, or Swift objects bridged to Objective-C with the@objc
prefix.
// Swift
expect(actual).to(beLessThan(expected))
expect(actual) < expected
expect(actual).to(beLessThanOrEqualTo(expected))
expect(actual) <= expected
expect(actual).to(beGreaterThan(expected))
expect(actual) > expected
expect(actual).to(beGreaterThanOrEqualTo(expected))
expect(actual) >= expected
// Objective-C
expect(actual).to(beLessThan(expected));
expect(actual).to(beLessThanOrEqualTo(expected));
expect(actual).to(beGreaterThan(expected));
expect(actual).to(beGreaterThanOrEqualTo(expected));
Values given to the comparison matchers above must implement
Comparable
.
Because of how computers represent floating point numbers, assertions
that two floating point numbers be equal will sometimes fail. To express
that two numbers should be close to one another within a certain margin
of error, use beCloseTo
:
// Swift
expect(actual).to(beCloseTo(expected, within: delta))
// Objective-C
expect(actual).to(beCloseTo(expected).within(delta));
For example, to assert that 10.01
is close to 10
, you can write:
// Swift
expect(10.01).to(beCloseTo(10, within: 0.1))
// Objective-C
expect(@(10.01)).to(beCloseTo(@10).within(0.1));
There is also an operator shortcut available in Swift:
// Swift
expect(actual) ≈ expected
expect(actual) ≈ (expected, delta)
(Type Option-x to get ≈ on a U.S. keyboard)
The former version uses the default delta of 0.0001. Here is yet another way to do this:
// Swift
expect(actual) ≈ expected ± delta
expect(actual) == expected ± delta
(Type Option-Shift-= to get ± on a U.S. keyboard)
If you are comparing arrays of floating point numbers, you'll find the following useful:
// Swift
expect([0.0, 2.0]) ≈ [0.0001, 2.0001]
expect([0.0, 2.0]).to(beCloseTo([0.1, 2.1], within: 0.1))
Values given to the
beCloseTo
matcher must be coercable into aDouble
.
// Swift
// Passes if instance is an instance of aClass:
expect(instance).to(beAnInstanceOf(aClass))
// Passes if instance is an instance of aClass or any of its subclasses:
expect(instance).to(beAKindOf(aClass))
// Objective-C
// Passes if instance is an instance of aClass:
expect(instance).to(beAnInstanceOf(aClass));
// Passes if instance is an instance of aClass or any of its subclasses:
expect(instance).to(beAKindOf(aClass));
Instances must be Objective-C objects: subclasses of
NSObject
, or Swift objects bridged to Objective-C with the@objc
prefix.
For example, to assert that dolphin
is a kind of Mammal
:
// Swift
expect(dolphin).to(beAKindOf(Mammal))
// Objective-C
expect(dolphin).to(beAKindOf([Mammal class]));
beAnInstanceOf
uses the-[NSObject isMemberOfClass:]
method to test membership.beAKindOf
uses-[NSObject isKindOfClass:]
.
// Passes if actual is not nil, false, or an object with a boolean value of false:
expect(actual).to(beTruthy())
// Passes if actual is only true (not nil or an object conforming to BooleanType true):
expect(actual).to(beTrue())
// Passes if actual is nil, false, or an object with a boolean value of false:
expect(actual).to(beFalsy())
// Passes if actual is only false (not nil or an object conforming to BooleanType false):
expect(actual).to(beFalse())
// Passes if actual is nil:
expect(actual).to(beNil())
// Objective-C
// Passes if actual is not nil, false, or an object with a boolean value of false:
expect(actual).to(beTruthy());
// Passes if actual is only true (not nil or an object conforming to BooleanType true):
expect(actual).to(beTrue());
// Passes if actual is nil, false, or an object with a boolean value of false:
expect(actual).to(beFalsy());
// Passes if actual is only false (not nil or an object conforming to BooleanType false):
expect(actual).to(beFalse());
// Passes if actual is nil:
expect(actual).to(beNil());
// Swift
// Passes if actual, when evaluated, raises an exception:
expect(actual).to(raiseException())
// Passes if actual raises an exception with the given name:
expect(actual).to(raiseException(named: name))
// Passes if actual raises an exception with the given name and reason:
expect(actual).to(raiseException(named: name, reason: reason))
// Passes if actual raises an exception with a name equal "a name"
expect(actual).to(raiseException(named: equal("a name")))
// Passes if actual raises an exception with a reason that begins with "a r"
expect(actual).to(raiseException(reason: beginWith("a r")))
// Passes if actual raises an exception with a name equal "a name"
// and a reason that begins with "a r"
expect(actual).to(raiseException(named: equal("a name"), reason: beginWith("a r")))
// Objective-C
// Passes if actual, when evaluated, raises an exception:
expect(actual).to(raiseException())
// Passes if actual raises an exception with the given name
expect(actual).to(raiseException().named(name))
// Passes if actual raises an exception with the given name and reason:
expect(actual).to(raiseException().named(name).reason(reason))
// Passes if actual raises an exception with a name equal "a name"
expect(actual).to(raiseException().withName(equal("a name")))
// Passes if actual raises an exception with a reason that begins with "a r"
expect(actual).to(raiseException().withName(withReason(beginWith(@"a r")))
// Passes if actual raises an exception with a name equal "a name"
// and a reason that begins with "a r"
expect(actual).to(raiseException().withName(equal("a name")).withReason(beginWith(@"a r")))
Note: Swift currently doesn't have exceptions. Only Objective-C code can raise exceptions that Nimble will catch.
// Swift
// Passes if all of the expected values are members of actual:
expect(actual).to(contain(expected...))
// Passes if actual is an empty collection (it contains no elements):
expect(actual).to(beEmpty())
// Objective-C
// Passes if expected is a member of actual:
expect(actual).to(contain(expected));
// Passes if actual is an empty collection (it contains no elements):
expect(actual).to(beEmpty());
In Swift
contain
takes any number of arguments. The expectation passes if all of them are members of the collection. In Objective-C,contain
only takes one argument for now.
For example, to assert that a list of sea creature names contains "dolphin" and "starfish":
// Swift
expect(["whale", "dolphin", "starfish"]).to(contain("dolphin", "starfish"))
// Objective-C
expect(@[@"whale", @"dolphin", @"starfish"]).to(contain(@"dolphin"));
expect(@[@"whale", @"dolphin", @"starfish"]).to(contain(@"starfish"));
contain
andbeEmpty
expect collections to be instances ofNSArray
,NSSet
, or a Swift collection composed ofEquatable
elements.
To test whether a set of elements is present at the beginning or end of
an ordered collection, use beginWith
and endWith
:
// Swift
// Passes if the elements in expected appear at the beginning of actual:
expect(actual).to(beginWith(expected...))
// Passes if the the elements in expected come at the end of actual:
expect(actual).to(endWith(expected...))
// Objective-C
// Passes if the elements in expected appear at the beginning of actual:
expect(actual).to(beginWith(expected));
// Passes if the the elements in expected come at the end of actual:
expect(actual).to(endWith(expected));
beginWith
andendWith
expect collections to be instances ofNSArray
, or ordered Swift collections composed ofEquatable
elements.
Like contain
, in Objective-C beginWith
and endWith
only support
a single argument for now.
// Swift
// Passes if actual contains substring expected:
expect(actual).to(contain(expected))
// Passes if actual begins with substring:
expect(actual).to(beginWith(expected))
// Passes if actual ends with substring:
expect(actual).to(endWith(expected))
// Passes if actual is an empty string, "":
expect(actual).to(beEmpty())
// Passes if actual matches the regular expression defined in expected:
expect(actual).to(match(expected))
// Objective-C
// Passes if actual contains substring expected:
expect(actual).to(contain(expected));
// Passes if actual begins with substring:
expect(actual).to(beginWith(expected));
// Passes if actual ends with substring:
expect(actual).to(endWith(expected));
// Passes if actual is an empty string, "":
expect(actual).to(beEmpty());
// Passes if actual matches the regular expression defined in expected:
expect(actual).to(match(expected))
// Swift
// with a custom function:
expect([1,2,3,4]).to(allPass({$0 < 5}))
// with another matcher:
expect([1,2,3,4]).to(allPass(beLessThan(5)))
// Objective-C
expect(@[@1, @2, @3,@4]).to(allPass(beLessThan(@5)));
For Swift the actual value has to be a SequenceType, e.g. an array, a set or a custom seqence type.
For Objective-C the actual value has to be a NSFastEnumeration, e.g. NSArray and NSSet, of NSObjects and only the variant which uses another matcher is available here.
In Nimble, matchers are Swift functions that take an expected
value and return a MatcherFunc
closure. Take equal
, for example:
// Swift
public func equal<T: Equatable>(expectedValue: T?) -> MatcherFunc<T?> {
return MatcherFunc { actualExpression, failureMessage in
failureMessage.postfixMessage = "equal <\(expectedValue)>"
return actualExpression.evaluate() == expectedValue
}
}
The return value of a MatcherFunc
closure is a Bool
that indicates
whether the actual value matches the expectation: true
if it does, or
false
if it doesn't.
The actual
equal
matcher function does not match when eitheractual
orexpected
are nil; the example above has been edited for brevity.
Since matchers are just Swift functions, you can define them anywhere: at the top of your test file, in a file shared by all of your tests, or in an Xcode project you distribute to others.
If you write a matcher you think everyone can use, consider adding it to Nimble's built-in set of matchers by sending a pull request! Or distribute it yourself via GitHub.
For examples of how to write your own matchers, just check out the
Matchers
directory
to see how Nimble's built-in set of matchers are implemented. You can
also check out the tips below.
actualExpression
is a lazy, memoized closure around the value provided to the
expect
function. The expression can either be a closure or a value directly
passed to expect(...)
. In order to determine whether that value matches,
custom matchers should call actualExpression.evaluate()
:
// Swift
public func beNil<T>() -> MatcherFunc<T?> {
return MatcherFunc { actualExpression, failureMessage in
failureMessage.postfixMessage = "be nil"
return actualExpression.evaluate() == nil
}
}
In the above example, actualExpression
is not nil
--it is a closure
that returns a value. The value it returns, which is accessed via the
evaluate()
method, may be nil
. If that value is nil
, the beNil
matcher function returns true
, indicating that the expectation passed.
Use expression.isClosure
to determine if the expression will be invoking
a closure to produce its value.
Using Swift's generics, matchers can constrain the type of the actual value
passed to the expect
function by modifying the return type.
For example, the following matcher, haveDescription
, only accepts actual
values that implement the Printable
protocol. It checks their description
against the one provided to the matcher function, and passes if they are the same:
// Swift
public func haveDescription(description: String) -> MatcherFunc<Printable?> {
return MatcherFunc { actual, failureMessage in
return actual.evaluate().description == description
}
}
By default, Nimble outputs the following failure message when an expectation fails:
expected to match, got <\(actual)>
You can customize this message by modifying the failureMessage
struct
from within your MatcherFunc
closure. To change the verb "match" to
something else, update the postfixMessage
property:
// Swift
// Outputs: expected to be under the sea, got <\(actual)>
failureMessage.postfixMessage = "be under the sea"
You can change how the actual
value is displayed by updating
failureMessage.actualValue
. Or, to remove it altogether, set it to
nil
:
// Swift
// Outputs: expected to be under the sea
failureMessage.actualValue = nil
failureMessage.postfixMessage = "be under the sea"
To use a custom matcher written in Swift from Objective-C, you'll have
to extend the NMBObjCMatcher
class, adding a new class method for your
custom matcher. The example below defines the class method
+[NMBObjCMatcher beNilMatcher]
:
// Swift
extension NMBObjCMatcher {
public class func beNilMatcher() -> NMBObjCMatcher {
return NMBObjCMatcher { actualBlock, failureMessage, location in
let block = ({ actualBlock() as NSObject? })
let expr = Expression(expression: block, location: location)
return beNil().matches(expr, failureMessage: failureMessage)
}
}
}
The above allows you to use the matcher from Objective-C:
// Objective-C
expect(actual).to([NMBObjCMatcher beNilMatcher]());
To make the syntax easier to use, define a C function that calls the class method:
// Objective-C
FOUNDATION_EXPORT id<NMBMatcher> beNil() {
return [NMBObjCMatcher beNilMatcher];
}
When supporting Objective-C, make sure you handle nil
appropriately.
Like Cedar,
most matchers do not match with nil. This is to bring prevent test
writers from being surprised by nil
values where they did not expect
them.
Nimble provides the beNil
matcher function for test writer that want
to make expectations on nil
objects:
// Objective-C
expect(nil).to(equal(nil)); // fails
expect(nil).to(beNil()); // passes
If your matcher does not want to match with nil, you use NonNilMatcherFunc
and the canMatchNil
constructor on NMBObjCMatcher
. Using both types will
automatically generate expected value failure messages when they're nil.
public func beginWith<S: SequenceType, T: Equatable where S.Generator.Element == T>(startingElement: T) -> NonNilMatcherFunc<S> {
return NonNilMatcherFunc { actualExpression, failureMessage in
failureMessage.postfixMessage = "begin with <\(startingElement)>"
if let actualValue = actualExpression.evaluate() {
var actualGenerator = actualValue.generate()
return actualGenerator.next() == startingElement
}
return false
}
}
extension NMBObjCMatcher {
public class func beginWithMatcher(expected: AnyObject) -> NMBObjCMatcher {
return NMBObjCMatcher(canMatchNil: false) { actualExpression, failureMessage, location in
let actual = actualExpression.evaluate()
let expr = actualExpression.cast { $0 as? NMBOrderedCollection }
return beginWith(expected).matches(expr, failureMessage: failureMessage)
}
}
}
Nimble can be used on its own, or in conjunction with its sister project, Quick. To install both Quick and Nimble, follow the installation instructions in the Quick README.
Nimble can currently be installed in one of two ways: using CocoaPods, or with git submodules. The master branch of
Nimble supports Swift 1.2. For Swift 1.1 support, use the swift-1.1
branch.
To use Nimble as a submodule to test your iOS or OS X applications, follow these 4 easy steps:
- Clone the Nimble repository
- Add Nimble.xcodeproj to the Xcode workspace for your project
- Link Nimble.framework to your test target
- Start writing expectations!
For more detailed instructions on each of these steps, read How to Install Quick. Ignore the steps involving adding Quick to your project in order to install just Nimble.
To use Nimble in CocoaPods to test your iOS or OS X applications, update CocoaPods to Version 0.36.0.
Then add Nimble to your podfile and add the use_frameworks!
line to enable Swift support for Cocoapods.
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
# Whatever pods you need for your app go here
target 'YOUR_APP_NAME_HERE_Tests', :exclusive => true do
use_frameworks!
# If you're using Swift 1.2 (Xcode 6.3 beta), use this:
pod 'Nimble', '~> 0.4.0'
# Otherwise, use this commented out line for Swift 1.1 (Xcode 6.2):
# pod 'Nimble', '~> 0.3.0'
end
Finally run pod install
.