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.