BDD testing in Swift with Sleipnir
- 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 -
Book
and the Library
tests and write for them. The class
Book
contains 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
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, 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
LibrarySpec
that inherits from the class SleipnirSpec
. It contains the main one context
and defines two exampla
that check the properties of the created class object Book
. A class object
Book
is 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
Library
using 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 !