BDD-style testing in Swift with Sleipnir

BDD-style testing in Swift with Sleipnir

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