BDD testing in Swift with Sleipnir

Original author: Artur Termenji
  • Transfer

Objective-C developers can use various frameworks for BDD testing their code.
Some of them:


With the advent of the Swift programming language, we decided to implement a BDD-style testing framework on pure Swift, without reference to Objective-C.
After a couple of weeks of implementation, we released the first public version of the Sleipnir framework .

Sleipnir was inspired by the Cedar framework and allows you to write BDD tests in this style:

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())
            }
        }
    }
}

The basic principles of Sleipnir


  • Sleipnir is independent NSObject, it's a BDD framework on pure Swift
  • Sleipnir does not use XCTest
  • Sleipnir displays test results on the command line in a convenient way and allows you to expand or supplement the output of results
  • Other features such as randomized test execution, focused / excluded test groups

We also found some alternative frameworks for BDD testing on Swift, for example Quick .
The choice between them is a matter of personal preference of the developer.

Usage example


We define two classes - Bookand the Librarytests and write for them.
The class Bookcontains information about the author and title of the book:
class Book {    
    var title: String
    var author: String
    init(title: String, author: String) {
        self.title = title
        self.author = author
    }   
}

Class Libraryis 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, we test the correctness of the class initialization Book:
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 created a class LibrarySpecthat inherits from the class SleipnirSpec. It contains the main one contextand defines two examplathat check the properties of the created class object Book.

A class object Bookis created in a block beforeAll{ }.
Sleipnir supports several test initialization and deinitialization blocks: beforeAll , afterAll , beforeEach and afterEach .

The result of calling all examplов( describe or context ) the top level in the test must be assigned to a variable for correct instance:
var book : () = context("Book") {  }

Now test the behavior of the class Library:
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 these tests will display the following information on the command line:
Running With Random Seed: 657464010
.......
Finished in 0.0091 seconds
7 examples, 0 failures

In the case of a failed test, detailed error information is displayed, including the 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

We tested the behavior of the class Libraryusing simple expectaions and matchers.
Sleipnir currently supports only three types of matchers: equal , beTrue and beFalse , however new ones will be added soon.

Future plans


Since this is the first public release, many features have not yet been implemented. We have an implementation plan for the near future, which includes:
  • Framework distribution mechanism
  • Support pending examples
  • Implementing Focusable / Excluded Test Groups
  • Xcode Templates
  • Support for shared examples
  • Syntax support should ( some_value should equal(some_another_value))
  • Wiki documentation
  • Testing Sleipnira with Sleipnir
  • Additional matchers, including:
    • beNil
    • beGreaterThan , beLessThan , beInRangeOf
    • asynchronous matchers ( will , willNot , after )
    • matchers for collections and strings ( contains , haveCount , beginWith , endWith , etc.)


Leave bug reports and feedback on github or in comments and stay tuned !

Also popular now: