- Test Driven Development (TDD) in Swift with Quick and Nimble
- By LAWRENCE TAN
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: 94 haox
- Proofread by: Swants, Atuooo
Writing test cases is not common in mobile development, and in fact, most mobile development teams avoid writing test cases as much as possible in order to speed up development.
As a “mature” developer, I’ve learned the benefits of writing test cases, not only to ensure that your app works as expected, but also to prevent other developers from changing your code by “locking” it. The link between the test code and the implementation code also makes it easier for new developers to understand and take on projects.
Test-driven Development (TDD)
Test-driven development (TDD) is like a new art of coding. It follows the following recursive loop:
- Write a test case that leads to failure
- Write some code to pass the above tests
- refactoring
- Repeat until we are satisfied
Let me show you a simple example. First consider implementing the following function:
func calculateAreaOfSquare(w: Int, h: Int) -> Double { }
Copy the code
Test 1: Given two numbers w=2 and h=2, the expected area should be 4. In this case, the test will fail because the function is not currently implemented.
Then we continued:
func calculateAreaOfSquare(w: Int, h: Int) -> Double { return w * h }
Copy the code
Test 1 has now passed! Wow!
Test 2: Given two numbers w=-1 and h=-1, the expected area should be 0. In this case, the test will fail because, based on the current implementation of the function, it will return 1.
Let’s move on:
func calculateAreaOfSquare(w: Int, h: Int) -> Double {
if w > 0 && h > 0 {
return w * h
}
return0}Copy the code
Test 2 has now passed too! Wow!
These operations can continue until you have dealt with all the edge cases. The next step is to refactor your code to make it look nice and clean while ensuring that all test cases pass.
Based on what we discussed above, we realized that TDD not only allows us to write high-quality code, it also allows us to deal with edge cases earlier. In addition, it can pair program with a different division of labor: one writing test cases, one writing implementation code. You can find out more about TDD at Dotariel’s Blog Post.
What will you learn in this tutorial?
By the end of the tutorial, you will have learned the following:
- Have a basic understanding of why TDD is great.
- Have a basic understanding of how Quick and Nimble work.
- Know how to use Quick and Nimble for UI testing.
- Know how to use Quick and Nimble for unit testing.
preparation
Before we go any further, here are some preliminary preparations:
- Swift3 environment and Xcode 8.3.3
- Experience in Swift and iOS development
Configure our project
Let’s say we want to develop an app that displays a list of movies. Start by opening Xcode and creating a single-view application called MyMovies. Check Unit Tests, and once we’ve configured the library and view controller, we’ll revisit the target.
Next, remove the existing viewcontrollers and recreate an inheritance in a new class of UITableViewController, named MoviesTableViewController.
Will be the Main. The storyboard ViewController deleted, drag a new UITableViewController to go in, let it inherited from MoviesTableViewController.
Then, change the cell style to Subtitle and the identifier to MovieCell so that we can show both the movie title and genre later.
Don’t forget to mark this view controller as Initial View Controller.
At this point, your code should look something 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
The movie data
Now, we need to create some movie data, and later, we need it to populate our view.
Genre Enum
enum Genre: Int {
case Animation
case Action
case None
}
Copy the code
This enumeration is used to mark the categories of movies.
Movie Struct
struct Movie {
var title: String
var genre: Genre
}
Copy the code
This movie data type is used to describe the movie data we need.
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
The Movie Data Assistant class helps us call the getMovies method directly, so we can get the data we need in a single call.
As a reminder, we haven’t done any TDD configuration in the project so far. Now let’s learn Quick and Nimble, the main parts of this tutorial!
Quick & Nimble
Quick is a testing framework built on TOP of XCTest, designed for Swift and Objective-C. It uses DSLS to write test cases that are very similar to RSpec.
Nimble, like Quick’s partner, provides the matcher as an assertion. For more information about it, check out here
Install Quick & Nimble with Carthage
As the Carthage library grew, I liked Carthage more than Cocoapods because it was more decentralized. Even if a library fails, the entire project can still compile successfully
#CartFile.private
github "Quick/Quick"
github "Quick/Nimble"
Copy the code
That’s what’s in cartfile.private, which I use to install dependencies. If you’re not familiar with Carthage, check it out first.
Drag cartfile.private into your project directory and then run the terminal Carthage Update. This command will clone dependencies, which you can find in Carthage -> Build -> iOS. Next, add both frameworks to the test project. You need to go to Build Phases and click on the plus sign in the upper left and select “New Copy Files Phase”. Set it to “Frameworks” and add both Frameworks.
Now all setup is done! Clap your hands and scatter flowers!
Write test case #1
Let’s start writing our first test case. What we know is that we have a list, some movie data. So, how do we make sure that the list view shows the correct number of items? Yes! We need to ensure that the number of cell rows in the list view is the same as the number of movie data. That’s the first thing we need to test. So get started! Go to MyMoviesTests, get rid of the XCTest code altogether, and bring in Quick and Nimble!
We have to make sure that our class is a subclass of QuickSpec, which of course is a subclass of XCTestCase. Be aware that Quick and Nimble are still XCTest based. Finally, one more thing we need to do is rewrite the spec() function, for which you can see set of example groups and examples.
import Quick
import Nimble
@testable import MyMovies
class MyMoviesTests: QuickSpec {
override func spec() {}}Copy the code
At this point, you need to understand that we will use it, Describe, and Context to write our tests. Describe and Context are just logical groupings of IT examples.
Test #1 – Expected list view rows = number of movie data
First, let’s introduce 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 have a @testable reference to MyMovies. This line of code marks which project we are testing and allows us to use the testable classes there. Since we need to test the controller’s view layer, we need to grab an instance from the storyboard.
The describe closure should be for MoviesTableViewController we write the first combination of test cases.
BeforeEach closures run before all examples in the Describe closure are executed. So in which you can write some need in MoviesTableViewController first test run.
_ = subject.view will put the view controller into memory, which is similar to calling viewDidLoad.
Finally, we can add test assertions after beforeEach {}. Such as:
context("when view is loaded") {
it("should have 8 movies loaded") {
expect(subject.tableView.numberOfRows(inSection: 0)).to(equal(8))
}
}
Copy the code
Let’s take it step by step. First, we have a composite example closure context marked when View is loaded; Then, we have another main example: It should have 8 movies loaded; We then expect or assert that the cell of the list view has eight rows. Run the Test case by pressing CMD+U or Product -> Test, and you should see the following information on the control panel:
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__should_have_8_movies_loaded]'Failed (0.009 seconds).Copy the code
So, you just wrote an imperfect test case. Start TDD!
Improve test case #1
Now, back to MoviesTableViewController, loading movie data! Then re-run the test case, and the test case you wrote earlier passes!
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
To summarize, first you write an imperfect test, then you perfect it with three lines of code, and the test passes, which is why we call it test-driven development (TDD), a way to ensure good and high quality code.
Write test case #2
Now, it’s time to wrap up the tutorial with a second test case. We realize that when we run the app, we’re just setting “title” and “subtitle” everywhere. But we didn’t verify that it was displaying our actual data! So, write a test case for the UI as well.
Enter the spec file. Add a new context and call it a Table View. Grab the first cell from the list view and test whether it displays the same data as it should.
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
After the test runs, you get the following failure message.
MoviesTableViewController__Table_View__should_show_movie_title_and_genre] : expected to equal <Animation>, got <Subtitle>
Copy the code
Come on, let’s improve the test case by presenting the cell with the corresponding data!
Improve test case #2
Since Genre is an enumeration, we need to add a different description to it. So we need to update the Movie class:
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
The cellForRow method also needs to be updated:
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
Wow! The second test case passed! At this point, let’s see if we can make the code clearer by refactoring, while still keeping the test cases passable. Remove empty functions and declare getMovies() as a calculated 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
Try it, rerun the test, it still passes.
conclusion
What did we do?
- The first test case we wrote to detect the number of movies failed
- Then we implemented the logic to load the movie, and the test passed
- To check that the correct data is displayed, we wrote a second test that failed
- Then we implemented the display logic, and the test passed
- We ended up stopping the testing and refactoring
That’s pretty much what TDD is all about. You can also try more with this project. If you have any questions about this tutorial, please let me know by leaving a comment below.
You can find the source code here
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.