Introduction
In Objective-C people use different frameworks to write BDD-style tests for their code. Some of them are:
With the inroduction of Swift we’ve decided to create BDD-style testing framework in pure Swift. After a couple weeks of implementation we are making the first public version of the framework called
Sleipnir. Sleipnir is highly inspired by
Cedar and allows you to write BDD-style tests in Swift:
class SampleSpec : SleipnirSpec {
var spec : () = describe("Horse") {
context("usual") {
it("is not awesome") {
let usualHorse = UsualHorse()
expect(usualHorse.legsCount).to(equal(4))
expect(usualHorse.isAwesome()).to(beFalse())
}
}
context("Sleipnir") {
it("is awesome") {
let sleipnirHorse = Sleipnir()
expect(sleipnirHorse.legsCount).to(equal(8))
expect(sleipnirHorse.isAwesome()).to(beTrue())
}
}
}
}
Core principles of Sleipnir
- Sleipnir is not dependent of
NSObject
, it is pure Swift BDD testing framework - Sleipnir is not using
XCTest
- Sleipnir has nice command line output and support for custom test reporters
- Other features, like seeded random tests invocation, focused and excluded examples/groups, etc.
We’ve found some other alternatives for Swift BDD testing, like
Quick. Choosing between different solutions is a matter of preference.
Usage sample
Let’s define two classes
Book
and
Library
and write tests for them.
Book
contains information about an author and a title of the book.
class Book {
var title: String
var author: String
init(title: String, author: String) {
self.title = title
self.author = author
}
}
Library
is a simple collection of books.
class Library {
var books: Book[]
init() {
self.books = Book[]()
}
func addBook(book: Book) {
books.append(book)
}
func removeLastBook() {
books.removeLast()
}
func clear() {
books.removeAll()
}
func size() -> Int {
return books.count
}
func hasBooks() -> Bool {
return size() > 0
}
func filterBy(#author: String) -> Book[] {
return books.filter { $0.author == author }
}
func filterBy(#title: String) -> Book[] {
return books.filter { !$0.title.rangeOfString(title).isEmpty }
}
}
First let’s test that instances of
Book
are initialized correctly:
class LibrarySpec : SleipnirSpec {
var book : () = context("Book") {
var swiftBook: Book?
beforeAll {
swiftBook = Book(title: "Introduction to Swift", author: "Apple Inc.")
}
it("has title") {
expect(swiftBook!.title).to(equal("Introduction to Swift"))
}
it("has author") {
expect(swiftBook!.author).to(equal("Apple Inc."))
}
}
}
We’ve created a
LibrarySpec
class which inherits from
SleipnirSpec
. It has a main context and two examples which check properties of a created
Book
instance.An instance of
Book
is created in
beforeAll{ }
block.Sleipnir supports several blocks for specs initialization:
beforeAll
,
afterAll
,
beforeEach
and
afterEach
. All the top-level example groups in a spec should be assigned to a variable in order to evaluate:
var book : () = context("Book") { }
Now let’s test a behaviour of a
Library
class:
class LibrarySpec : SleipnirSpec {
...
var library : () = context("Library") {
var swiftLibrary: Library?
beforeAll {
swiftLibrary = Library()
}
afterAll {
swiftLibrary = nil
}
describe("empty") {
it("has no books") {
expect(swiftLibrary!.hasBooks()).to(beFalse())
}
}
describe("with books") {
beforeEach {
swiftLibrary!.addBook(Book(title: "Introduction to Swift", author: "Apple Inc."))
swiftLibrary!.addBook(Book(title: "Using Swift with Cocoa", author: "Apple Inc."))
swiftLibrary!.addBook(Book(title: "Swift tutorials", author: "John Doe"))
swiftLibrary!.addBook(Book(title: "Programming iOS with Swift", author: "Vladimir Swiftin"))
}
afterEach {
swiftLibrary!.clear()
}
it("is not empty") {
expect(swiftLibrary!.hasBooks()).to(beTrue())
}
it("has correct number of books") {
expect(swiftLibrary!.size()).to(equal(4))
swiftLibrary!.removeLastBook()
expect(swiftLibrary!.size()).to(equal(3))
}
describe("filters books") {
it("by author") {
expect(swiftLibrary!.filterBy(author: "Apple Inc.").count).to(equal(2))
}
it("by title") {
expect(swiftLibrary!.filterBy(title: "tutorials").count).to(equal(1))
}
}
}
}
}
Running those specs will produce the following command line output:
Running With Random Seed: 657464010
.......
Finished in 0.0091 seconds
7 examples, 0 failures
In case of a failed example you will see a detailed information about the failure including file and line number:
Running With Random Seed: 2027508247
..F....
FAILURE Library with books has correct number of books:
/Users/atermenji/Coding/objc/Sleipnir/Sample/LibrarySpec.swift:64 Expected 3 to equal [2]
Finished in 0.0043 seconds
7 examples, 1 failures
As you can see we’ve tested the behaviour of a
Library
class using simple expectations and matchers. Sleipnir currently supports only three matchers:
equal
,
beTrue
and
beFalse
, but more of them will be added soon.
What’s next
Since this is a first public release, a lot of features are not yet implemented. We have a roadmap for the nearest future which includes:
- Distribution as a framework
- Pending examples support
- Focused and excluded examples/groups support implementation
- XCode templates
- Shared examples support
should
syntax support- Specs for Sleipnir using Sleipnir
- Wiki documentation
- More matchers, including:
beNil
beGreaterThan
, beLessThan
, beInRangeOf
- asynchronous matchers (
will
, willNot
, after
) - matchers on collections/strings (
contains
, haveCount
, beginWith
, endWith
, etc.)
You may post your ideas or issues to the
Sleipnir repo.
Stay tuned for updates