Writing Unit Tests With Quick

Dejan Agostini
Writing Unit Tests With Quick
Writing unit tests might be boring and tedious work, but like your vegetables, it's good for you :) You realise how valuable unit tests are when you're implementing a new feature on an old piece of code. And, as you know, unit tests might not be as readable as your production code. There's certainly a lot of duplication in them. Fortunately there's a great little library that's tackling this problem, it's called Quick. In this article we'll talk about writing unit tests with Quick.

About Quick

Quick is a behaviour driven development framework. If you've never encountered BDD before it's simply a way of organising your tests using natural language constructs that makes the tests way easier to read. You can read a lot more on BDD on wikipedia. For what it's offering, quick is a surprisingly simple library and it's incredibly easy to get used to. All Quick is doing is providing you with a closure based syntax for your assertions. Speaking of assertions. The assertions are handled by another library called 'Nimble' which makes assertions a lot more readable. And writing async code with Nimble is as simple as it gets. You don't have to use Nimble for assertions, you can simple use your standard 'XCTAssert' instead. If that's what you prefer. We could write a whole other article on Nimble, so let's get back on track.

The Usual Tests

Here's how your typical test might look like:
SwiftEnigmaTests.swift
import XCTest
@testable import ATEnigma

class EnigmaTests: XCTestCase {
    
    func testEncoding() {
        let config = SpindleConfiguration(plugboardMappings: [Mapping(from: 23, to: 8), Mapping(from: 1, to: 10), Mapping(from: 15, to: 16), Mapping(from: 3, to: 13), Mapping(from: 5, to: 20), Mapping(from: 0, to: 21)],
                                          rotors: [2, 1, 5],
                                          rotorSettings: [11, 5, 19])
        
        let encodingEnigma = EnigmaMachine(config)
        let decodingEnigma = EnigmaMachine(config)
        
        let encodedString = encodingEnigma.encode("Hello World.")
        print("Encoded string: ", encodedString)
        let decodedString = decodingEnigma.decode(encodedString)
        
        XCTAssertEqual(decodedString, "HELLO WORLD.")
    }
    
    func testEncodingLong() {
        let message = "LOREM IPSUM DOLOR SIT AMET CONSECTETUR ADIPISCING ELIT SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA. UT ENIM AD MINIM VENIAM QUIS NOSTRUD EXERCITATION ULLAMCO LABORIS NISI UT ALIQUIP EX EA COMMODO CONSEQUAT. DUIS AUTE IRURE DOLOR IN REPREHENDERIT IN VOLUPTATE VELIT ESSE CILLUM DOLORE EU FUGIAT NULLA PARIATUR. EXCEPTEUR SINT OCCAECAT CUPIDATAT NON PROIDENT SUNT IN CULPA QUI OFFICIA DESERUNT MOLLIT ANIM ID EST LABORUM."
        
        let config = SpindleConfiguration(plugboardMappings: [Mapping(from: 23, to: 8), Mapping(from: 1, to: 10), Mapping(from: 15, to: 16), Mapping(from: 3, to: 13), Mapping(from: 5, to: 20), Mapping(from: 0, to: 21)],
                                          rotors: [2, 1, 5],
                                          rotorSettings: [11, 5, 19])
        
        let encodingEnigma = EnigmaMachine(config)
        let decodingEnigma = EnigmaMachine(config)
        
        let encodedString = encodingEnigma.encode(message)
        print("Encoded: ", encodedString)
        let decodedString = decodingEnigma.decode(encodedString)
        
        XCTAssertEqual(decodedString, message)
    }
}
Nothing unusual there. If the tests fail it might take you a few seconds to realise why. The failed assertion would point you to the exact test method that failed and then it's up to you to realise from the context of the test why it failed. Quick can help you with that.

Using Quick

Quick is available through cocoapods, so adding it to your project should be pretty trivial. Just remember to add pods to your test target in the podspec file:
RubyPodfile
target 'ATEnigma' do
  use_frameworks!

  target 'ATEnigmaTests' do
    inherit! :search_paths
    pod 'Quick'
    pod 'Nimble'
  end

end
Let's convert the tests in the code snippet from above to Quick.
SwiftEnigmaTestsQ.swift
import Quick
import Nimble
@testable import ATEnigma

class EnigmaTestsQ: QuickSpec {
    
    override func spec() {
        
        var config: SpindleConfiguration!
        beforeSuite {
            config = SpindleConfiguration(plugboardMappings: [Mapping(from: 23, to: 8), Mapping(from: 1, to: 10), Mapping(from: 15, to: 16), Mapping(from: 3, to: 13), Mapping(from: 5, to: 20), Mapping(from: 0, to: 21)],
                                          rotors: [2, 1, 5],
                                          rotorSettings: [11, 5, 19])
        }
        
        describe("enigma") {
            
            var encodingEnigma: Enigma!
            var decodingEnigma: Enigma!
            beforeEach {
                encodingEnigma = EnigmaMachine(config)
                decodingEnigma = EnigmaMachine(config)
            }
            
            context("when it's decrypting a message", {
                it("returns an uppercased message") {
                    let encodedString = encodingEnigma.encode("Hello World.")
                    let decodedString = decodingEnigma.decode(encodedString)
                    expect(decodedString).to(equal("HELLO WORLD."))
                }
            })
            
            context("when it's decrypting a long message", {
                it("decrypts it correctly") {
                    let message = "LOREM IPSUM DOLOR SIT AMET CONSECTETUR ADIPISCING ELIT SED DO EIUSMOD TEMPOR INCIDIDUNT UT LABORE ET DOLORE MAGNA ALIQUA. UT ENIM AD MINIM VENIAM QUIS NOSTRUD EXERCITATION ULLAMCO LABORIS NISI UT ALIQUIP EX EA COMMODO CONSEQUAT. DUIS AUTE IRURE DOLOR IN REPREHENDERIT IN VOLUPTATE VELIT ESSE CILLUM DOLORE EU FUGIAT NULLA PARIATUR. EXCEPTEUR SINT OCCAECAT CUPIDATAT NON PROIDENT SUNT IN CULPA QUI OFFICIA DESERUNT MOLLIT ANIM ID EST LABORUM."
                    let encodedString = encodingEnigma.encode(message)
                    let decodedString = decodingEnigma.decode(encodedString)
                    expect(decodedString).to(equal(message))
                }
            })
        }
    }
}
You can see the same two unit tests here. But they're a lot more readable. The amount of code and the testing logic is still going to be roughly the same. Quick simple adds closures around the tests. 'describe', 'context' and 'it' are the main closures you'll be using in your tests. You also have closures that run before the whole suite of tests and before each test. This will help to reduce the code duplication in your tests. The 'expect' function that you see in the code snippet above is from the 'Nimble' framework. Nimble is a very elegant way to test your code and testing asynchronous code is as simple as testing synchronous code. We'll see an example of it later. Nimble is a matcher framework. In the code snippet above the 'equal' function is just one of many matchers that we can use. Nimble has matchers for collections, strings even errors and exceptions. It's a great little framework and it comes as a separate pod, so you can use it on its own if you like. If you didn't use Quick, then for every test you wanted to run you would have to write a separate function and the class would get less readable. With quick you have a nice way of logically organising your tests. With the closures that you're using Quick will actually generate test functions for you under the hood. That means that when your tests fail they're a lot more readable:

Asynchronous Tests

Testing asynchronous code is usually a pain because you're forced to write a lot of boilerplate code. With Nimble testing async code turns into a one liner:
SwiftAsyncTestsQ.swift
import Quick
import Nimble

class AsyncTestsQ: QuickSpec {
    
    override func spec() {
        
        var asyncClass: DummyAsyncClass!
        beforeSuite {
            asyncClass = DummyAsyncClass()
        }
        
        describe("async class") {
            it("should return a value over time", closure: {
                var result: String?
                asyncClass.doSomethingLater(completion: { (value) in
                    result = value
                })
                expect(result).toEventually(equal("Hello"), timeout: 5, pollInterval: 0.1, description: "Periodical check for the result")
            })
        }
    }
}
You specify the timeout and the poll interval and that's it. The method signature should give you a hint into how Nimble is asserting in the background. It simply polls the variable periodically until the assertion is true of until it times out.

Conclusion

When you get into a habit of writing unit tests you'll end up with a lot of them. Eventually they'll start failing, like they should. Once you start digging into the test failures, any help you can get is more than welcome. This is where Quick comes in. I've found it to be really helpful and very easy to get used to. Quick forces you to think how to structure your tests, to think about the context and expected results. And once you start writing your tests they actually read almost like a book. This is definitely a framework you should try out and see if it works for you. It will save you loads of time when writing and when reading your tests. If you want to learn more about writing unit tests, check out this article on writing unit tests using dependency injection or this article on when and why to write unit tests. You can find the example project on the GitLab repo along with all the code snippets. I hope you've found the article useful and that you had fun reading it :) As usual... Have a nice day :) ~D;

More resources