This is not a popular option for writing tests of any kind on mobile applications, and in fact, most mobile application development teams try to avoid writing tests as much as possible, hoping that tutorials will save time and speed up the development process.

Thinks he is a mature technology developers, I deeply experience the benefits of writing tests, not only ensure the function of the application works as expected, still can lock your own code, to prevent other developers to change the code, test and the coupling between the code can help new developers to easily onboard or take over the project.

Test-driven Development

Test-driven Development (TDD) is like the new art of writing code. It follows the following cycle:

  • Start by writing a test that fails
  • Patch in the code and make it pass the test
  • Refactor
  • Repeat until you are satisfied

Here provides a simple example for readers, please refer to the following operation example:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { }Copy the code

Test 1:

Given w=2 and h=2, the expected output would be 4. In the above code, the test result would be fail because we haven’t implemented it yet.

Next, we add some code:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }Copy the code

The first test should be passed now!

(adsbygoogle = window.adsbygoogle || []).push({});Copy the code

Test 2:

Given w=-1 and h=-1, the expected area calculation should be 0. In this example, the test fails again because the current way of executing the function is that the output is 1.

Next, we add some code:

func calculateAreaOfSquare(w: Int, h: Int) -> Double { 
    if w > 0 && h > 0 { 
        return w * h 
    } 

    return0}Copy the code

And now I’ve passed the second test. Fantastic!

Continue this action until all edge cases are handled, while refactoring to make the code better and pass all tests.

Based on what we’ve discussed so far, we know that TDD not only leads to better quality code, but also allows developers to deal with extreme situations ahead of time. In addition, it allows two developers to pair programming efficiently, with one engineer writing the tests and the other writing the code that will pass the tests. You can learn more about this in Dotariel’s blog post.

What will you learn in this tutorial

By the end of this tutorial, you should be able to take away the following:

  • Have a basic understanding of why TDD is good.
  • Basic understanding of Quick & Nimble.
  • Learn how to write a UI test using Quick & Nimble.
  • Learn how to write a Unit Test using Quick & Nimble.

The preparatory work

Before getting to the focus of this article, here are some development environment preparations:

  • Xcode 8.3.3 is installed and developed using Swift 3.1
  • Some experience in Swift and iOS development

What project?

Let’s say we’ve been assigned the task of developing a simple movie Application that displays movie information. Start Xcode and create a new Single View Application called MyMovies with Unit Tests checked. When the libraries and view controllers are set, we will revisit the target.

TDD Sample Project

Next, let’s remove the original viewcontrollers and dragged into a UITableViewController, it named MoviesTableViewController, in the Main. The storyboard, Delete the ViewController, and pull it into a new TableViewController, and set the category to MoviesTableViewController. Now we set the prototype cell’s style to Subtitle and identifier to MovieCell so that we can later display the title and genre of the movie.

Remember to set the View Controller to Initial View Controller, as shown below.

So far, your code should look like this:

import UIKit

class MoviesTableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // MARK: - Table view data source
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return0}}Copy the code

Movies

Now, let’s create the movie data so we can use it later to populate our view.

Genre Enum

enum Genre: Int {
    case Animation
    case Action
    case None
}Copy the code

This enumeration (enum) is used to determine our movie type.

Movie Struct

struct Movie {
    var title: String
    var genre: Genre
}Copy the code

This movie data type is used to represent our individual movie data.

class MoviesDataHelper {
    static func getMovies() -> [Movie] {
        return [
            Movie(title: "The Emoji Movie", genre: .Animation),
            Movie(title: "Logan", genre: .Action),
            Movie(title: "Wonder Woman", genre: .Action),
            Movie(title: "Zootopia", genre: .Animation),
            Movie(title: "The Baby Boss", genre: .Animation),
            Movie(title: "Despicable Me 3", genre: .Animation),
            Movie(title: "Spiderman: Homecoming", genre: .Action),
            Movie(title: "Dunkirk", genre: .Animation)
        ]
    }
}Copy the code

This MoviesDataHelper class helps us call the getMovies method directly so that we can get movie data in a single call.

It is important to note that at this stage, no TDD has been executed, as it is still being planned for the project. Now let’s move on to the main content of this tutorial, Quick & AMP; Nimble!

Quick & Nimble

Quick is a test development framework built on XCTest, supports Swift and Objective-C, and provides a DSL to write tests, much like RSpec.

Nimble is like Quick’s buddy. Nimble offers Matcher as an Assertion. For more information on the framework, check out this link.

Install Quick & Nimble with Carthage

As Carthage has evolved, I like Carthage more than Cocoapods because it’s more decentralized, and when one of the frameworks doesn’t build, the whole project can still compile.

#CartFile.private
github "Quick/Quick"
github "Quick/Nimble"Copy the code

The above is cartfile.private, which is used to install my dependencies. If you don’t have any experience with Carthage, please check this link.

Place cartfile.private in the folder and then run Carthage Update, which will clone this dependencies and the reader should get both frames in your Carthage -> Build -> iOS folder. Then, add the two frames to the two test targets. Next, go to Build Phases, click the plus sign in the upper left corner, and select “New Copy Files Phase”. Set destination to “Frameworks”. And add two frames to it.

Let’s go! You have now set up all the test libraries for this article!

Write our Test #1

Let’s start by writing the first test. We all know that we have a list and we have some movie data. How do we make sure that the column view displays the right number of items? That’s right! We need to make sure that the Row of our TableView matches the amount of movie data we have. That’s our first test, so now go to our MyMoviesTests, delete the XCTest code and import our Quick and Nimble suites!

The last thing we need to do here is declare a Override function spec(), Here we use to define a set of Example Groups and Examples.

import Quick
import Nimble

@testable import MyMovies

class MyMoviesTests: QuickSpec {
    override func spec() {}}Copy the code

In this case, we will use a lot of it, DESCRIBE, and Context to write our tests. Where each IT represents a small set of tests, describe and Context are logical groupings of IT examples that describe what you are testing.

Test #1 — Expect TableView Rows Count = Movies Data Count

First, let’s introduce our Subject, which is our view controller.

import Quick
import Nimble

@testable import MyMovies

class MyMoviesTests: QuickSpec {
    override func spec() {
        var subject: MoviesTableViewController!

        describe("MoviesTableViewControllerSpec") {
            beforeEach {
                subject = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "MoviesTableViewController") as! MoviesTableViewController

                _ = subject.view
            }
        }
    }
}Copy the code

Note that we put @testable MyMovies here, which basically flags the project we are testing and allows us to import classes from there. When we test the view layer of TableViewController, we need to get an instance from the storyboard.

The describe closure (closure) to start my first test case, write tests for MoviesTableViewController.

BeforeEach closures are executed in the describe closure, it will be in each sample before you start running, so you can see it as an inside MoviesTableViewController before each test is carried out, would run this code first.

_ = subject.view puts the view controller into memory, which is like calling viewDidLoad.

Finally, we can add our test assertion after beforeEach {} as follows:

context("when view is loaded") {
    it("should have 8 movies loaded") {
        expect(subject.tableView.numberOfRows(inSection: 0)).to(equal(8))
   }
}Copy the code

From the grouped example closure, we got a context. It was a grouped example closure, which was labeled when view is loaded. The main example is that it should have 8 movies loaded. We can predict that our table view will have 8 rows. Now let’s press CMD + U to run the Test, or follow the Product -> Test path. After a few seconds you will get an alert message in the console:

MoviesTableViewController__when_view_is_loaded__should_have_8_movies_loaded] : expected to equal <8>, got <0>

Test Case '-[MyMoviesTests.MoviesTableViewControllerSpec MoviesTableViewController__when_view_is_loaded__shouCopy the code

So you just wrote a failed test, let’s fix it, and get to TDD!

Fix Test #1

We went back to the main MoviesTableViewController and load our movie data! After you add the code, run the test again, and congratulate yourself for passing your first test!

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return MoviesDataHelper.getMovies().count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")
    return cell!
}Copy the code

For review, you just wrote a test that failed, fixed it with three lines of code, and now it passes. This is what we call TDD, the way to ensure high-quality, good Codebas.

Write our Test #2

Now it’s time to end this tutorial with a second test case. If we run the application and just set “title” and “subtitle” everywhere, we’re missing the actual movie data! Write a test for the UI to do this!

Take a look at our spec file. Introduce a new context call to Table View. Fetch the first cell from the table view and test if the data matches.

context("Table View") {
    var cell: UITableViewCell!

    beforeEach {
            cell = subject.tableView(subject.tableView, cellForRowAt: IndexPath(row: 0, section: 0))
    }

    it("should show movie title and genre") { expect(cell.textLabel? .text).to(equal("The Emoji Movie"))
        expect(cell.detailTextLabel?.text).to(equal("Animation"))}}Copy the code

Now running the tests will see them fail.

MoviesTableViewController__Table_View__should_show_movie_title_and_genre] : expected to equal <Animation>, got <Subtitle>Copy the code

Again, we need to fix this test! We need to display the correct data for our cell Labels.

Fix Test #2

We previously used Genre as an enum to expand more code here, so update Movie by referring to the code below:

struct Movie {
    var title: String
    var genre: Genre

    func genreString() -> String {
        switch genre {
        case .Action:
            return "Action"
        case .Animation:
            return "Animation"
        default:
            return "None"}}}Copy the code

Update our cellForRow method here:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")

    letmovie = MoviesDataHelper.getMovies()[indexPath.row] cell? .textLabel? .text = movie.title cell? .detailTextLabel?.text = movie.genreString()return cell!
}Copy the code

Steady! You just passed your second test case! At this point, let’s look at what we can refactor, try to make the code cleaner, but still pass all the tests,

We delete the empty functions and declare our getMovies() as computed Property.

class MoviesTableViewController: UITableViewController {

    var movies: [Movie] {
        return MoviesDataHelper.getMovies()
    }

    // MARK: - Table view data source
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return movies.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell")

        letmovie = movies[indexPath.row] cell? .textLabel? .text = movie.title cell? .detailTextLabel?.text = movie.genreString()returncell! }}Copy the code

If you run the tests again, all tests should still pass, try it!

conclusion

So what have we accomplished?

  • We wrote the first test to check the number of movies and let itfail.
  • We implement the logic to load the movie and then let itpass.
  • We wrote a second test to check that it was displayed correctly, and let itfail.
  • We implement the display logic and then let the testpass.
  • Then suspend the test work and continuerefactor.

This is generally how TDD works, and you can continue to use this project to try out more testing work. If you have any questions about this tutorial, please let me know in the comments.

For the sample project, you can download the full Source code on GitHub.

译 文 : Test Driven Development (TDD) in Swift with Quick and Nimble