You’re working on a new feature and suddenly an old feature stops working even though you wrote unit tests. Or you’re refactoring legacy code and you think it’s done, but suddenly you find a lot of bugs. So you go back, you make the fixes, and you think it’s done, but then you find more bugs. You repeat and you think it’s done every time, but the same thing happens, every time.

You have to write tests to make sure that your product is working, and the only way to prove it’s working is through testing. And you need to be testing constantly in order to catch regression errors so that you can add new features without breaking existing ones.

So you’re working on software with tests and yet you have a bug in the logic that was already covered by those tests. ?







Your test results have given you a false positive about your software.

Test-Driven Development (TDD) is the best solution for this problem. TDD is not unit testing. Unit testing is writing code to test our code. So first, we write our code, and then write unit tests to verify our logic that we just wrote. But TDD is totally different. With TDD, you write the tests first before writing the code.

TDD forces you to see your failing tests, which shows you how to turn them into green when you fix these failed tests.

Test-Driven Development Definition

TDD is a development technique where you must first write a test that fails, then write code to get it working, and finally you will need to refactor the code to be as simple as possible.

In many cases, writing automated tests is seen as not real work and boring compared to building new features. TDD, however, turns testing into a design activity. We use the tests we write to clarify our ideas about what we want the code to do. It also keeps code as simple as possible so it’s easier to understand and modify, especially since developers spend more time reading code than writing it.

As we develop in TDD, it gives us feedback about the quality of both its implementation (does it work) and design (is it well structured).

Each level of tests should answer specific questions about our code.

1. End-to-End Tests: Does the whole system work?

2. Integration Tests: Do our objects work with each other correctly?

3. Unit Tests: Do our objects do the right thing?

TDD Process

First we need to write user stories, which describe the system in detail. User stories describe business features in a convenient way for technical teams so that they can work on them easily.

More important than user stories is their acceptance criteria, which you use to validate that each story is done or not. Solid tests start from writing solid acceptance criteria.

For each acceptance criteria, you need to write a failed end-to-end test. You need to write code to make this end-to-end test pass, by writing failed integration tests that describe how your objects will interact with each other. For every class, you need to write a failed unit test to test the output of each single unit. Then you need to write actual code to make the unit tests pass for all classes, and the integration tests and end-to-end tests pass for all acceptance criteria.





Example TDD Project

We are going to build an app that shows a list of movies (name and rating). Even a small story (feature) like this is too large to write in one go, so we need to figure out roughly the steps we might take to get there.

Here are all the features required for our sample application:

Show a list of movies. ??

Here is our user story:

As a user, I want to be able to see a list view so that I can choose my next movie.

And here is our acceptance criteria:

The app should show the name and rating of each movie.

Now we can start. ?

Setting Up Our Project

First, you need to create a single view project with Unit Tests and UI Tests targets:

Most App Store apps communicate with the cloud, fetch data, and show it to the user. In order to work on a solid base on which to build features, you need to simulate the two sides that will communicate with your system. The first side is user interaction, which can be done using UI tests that trigger user events as if from an actual user. As for the cloud, you can use a framework like Swifter to simulate server requests.

Swifter Wrapper

Swifter is a tool that will help you stub application requests and return the desired response to facilitate testing and prevent test flakiness.

We are going to make a wrapper class over Swifter to facilitate stubbing network requests.

We will create a class called HTTPDynamicStubs, which will provide a public method. You just need to add the URL you want to stub and the link to the JSON file (more on this later) to be returned when your app requests this API and the HTTPMethod .

Before every test case, all you need to do is create an instance from HTTPDynamicStubs and call a setup method, which will start the server and change the base URL to localhost . To remove stubs, tearDown the HTTPDynamicStubs after each test.

override func setUp() { continueAfterFailure = false dynamicStubs.setUp() app.launchEnvironment = ["BASEURL" : "http://localhost:8080"] continueAfterFailure = false super.setUp() } override func tearDown() { super.tearDown() dynamicStubs.tearDown() }



Inside each test case, you can stub requests and return the JSON you need.

func testMoviesList() { dynamicStubs.setupStub(url: "/Movies", filename: "listOfMovies", method: .GET) app.launch() }

User Story

As I said before, solid TDD starts from writing good user stories with clear acceptance criteria. We will represent each user story with an end-to-end UI test.

Note: You cannot write end-to-end tests for everything. If you do, you’ll end up with very slow tests that you won’t even be able to run every time.

To recap, here’s our user story:

As a user, I want to be able to see a list view so that I can choose my next movie.

If you would like to follow along with this tutorial, I prepared a JSON file that contains a list of movies that will help us test this user story.

[ { "name": "Avengers: Infinity War", "rating": "8.5", "desc": "The Avengers and their allies must be willing to sacrifice all in an attempt to defeat the powerful Thanos before his blitz of devastation and ruin puts an end to the universe.", }, { "name": "Bohemian Rhapsody", "rating": "8.4", "desc": "A chronicle of the years leading up to Queen's legendary appearance at the Live Aid (1985) concert.", }, { "name": "Aquaman", "rating": "8.3", "desc": "Arthur Curry learns that he is the heir to the underwater kingdom of Atlantis, and must step forward to lead his people and be a hero to the world.", }, { "name": "A Star Is Born", "rating": "8.2", "desc": "A musician helps a young singer find fame, even as age and alcoholism send his own career into a downward spiral.", }, ]

Writing Our End-to-End Test

According to our user story, a user will need to open the application and see the list of movies. According to our acceptance criteria, each movie will have a name and rating. So I need to write a test that will launch the application and make sure that there is a tableView filled with movies, with the name and rating of each movie.

func testShowingMovieNameAndImageName() { dynamicStubs.setupStub(url: "/Movies", filename: "listOfMovies", method: .GET) app.launch() let tablesQuery = app.tables // Assert on first movie XCTAssertTrue(tablesQuery.cells.staticTexts["Avengers: Infinity War"].exists, "Failure: did not show the first movie name.") XCTAssertTrue(tablesQuery.cells.staticTexts["8.5"].exists, "Failure: did not show the first movie rating.") // Assert on second movie XCTAssertTrue(tablesQuery.cells.staticTexts["Bohemian Rhapsody"].exists, "Failure: did not show the second movie name.") XCTAssertTrue(tablesQuery.cells.staticTexts["8.4"].exists, "Failure: did not show the second movie rating.") // Assert on third movie XCTAssertTrue(tablesQuery.cells.staticTexts["Aquaman"].exists, "Failure: did not show the third movie name.") XCTAssertTrue(tablesQuery.cells.staticTexts["8.3"].exists, "Failure: did not show the third movie rating.") // Assert on fourth movie XCTAssertTrue(tablesQuery.cells.staticTexts["A Star Is Born"].exists, "Failure: did not show the fourth movie name.") XCTAssertTrue(tablesQuery.cells.staticTexts["8.2"].exists, "Failure: did not show the fourth movie rating.") }

Line 1: Stub the network request for movies and return the mock JSON file.

Lines 3, 4, 5: Assert on each movie name and rating inside UITableView .

After writing the end-to-end test, you should make the test fail. Read the assertion messages and ask yourself if you really understand the problems from reading the messages. If not, rewrite more meaningful messages. This will help you identify problems later when you change some code and this test fails.

Acceptance Criteria

Now it’s time to write the integration test for our acceptance criteria. To recap, here is our acceptance criteria:

The app should show the name and rating of each movie.

When writing integration tests, imagine the components of your system that will be responsible for implementing this part of the feature, and how they will communicate with each other.

Integration tests exclude the UI component and only test functionality after the UI layer (it’s supposed to be fast). If you are going to use an MVP architectural pattern in your app, your integration tests will make sure that the presentation layer will communicate properly with the model layer.

So, we will trigger an action from the presentation layer, stub the network request, and assert the callback to make sure that collaboration works between the presentation, model, and network layers.

Writing Our Integration Test

With MVP, View will be responsible for displaying the data and listening to user interactions while Model will be responsible for defining the business logic and fetching, updating, and inserting the data. Presenter will make sure to decouple interaction between Model and View, and act as the communication layer between them.







Now I need to imagine how many objects I will have, the responsibility of each object, and how they will interact with each other.

So I think I will need MovieListPresenter , which will be responsible for asking the MovieListModel to fetch the movies. MovieListModel will fetch them through the network layer, receive the JSON, parse the JSON, and return them to the presenter.

func testShowingMovieNameAndImageName() { let networkLayer = NetworkLayerMock(mockedData: [["name":"Avengers: Infinity War", "rating": "8.5"], ["name":"Bohemian Rhapsody", "rating": "8.4"]]) let moviesListModel = MoviesListModel(networkLayer: networkLayer) let moviesListPresenter = MoviesListPresenter(moviesListModel: moviesListModel) let viewControllerMock = ViewControllerMock() moviesListPresenter.delegate = viewControllerMock moviesListPresenter.fetchMovies() XCTAssertTrue(viewControllerMock.didFetchMovies, "Success in fetching movies") XCTAssertTrue(moviesListPresenter.movieName(index:0) == "Avengers: Infinity War", "First movie name is not as expected") XCTAssertTrue(moviesListPresenter.movieRating(index:0) == "8.5", "First movie rating is not as expected") XCTAssertTrue(moviesListPresenter.movieName(index:1) == "Bohemian Rhapsody", "Second movie name is not as expected") XCTAssertTrue(moviesListPresenter.movieRating(index:1) == "8.4", "Second movie rating is not as expected") }

This integration test will ask MoviesListPresenter to fetch movies. It is supposed to receive a callback when it successfully receives a response or the request fails. So this test needs to mock the network layer and simulate that an actual request came from the cloud.

Steps:

I need to create a mock network layer, which will return specific responses so I can assert the returned data from Presenter. I need to create MoviesListModel , which will be responsible for calling the network layer, receiving a response, and parsing the data. Presenter will use Model to fetch movies and reformat the movies to be displayed on UI components.

Note: Here I should not be able to receive objects from MoviesListPresenter . It’s only supposed to return primitive data types to totally isolate Model from View.

Network Layer

For the sake of this example project, we are not going to implement a complete network layer. We will build the most simple block of code to execute a GET request and return a response.

open func executeGETRequest(api:String, completionBlock:@escaping (Data?) -> Void) { let environment = ProcessInfo.processInfo.environment var baseUrl:String = "" if let _ = environment["isUITest"] { // Running in a UI test baseUrl = ProcessInfo.processInfo.environment["BASEURL"]! } else { baseUrl = "https://api.example.com" } guard let gitUrl = URL(string: baseUrl + api) else { return } let session = URLSession(configuration: URLSessionConfiguration.default) let dataTask = session.dataTask(with: gitUrl) { (data, response, error) in guard let data = data else { return } do { if let returnData = String(data: data, encoding: .utf8) { print(returnData) } else { print("empty") } } if let err = error { print("Err", err) } completionBlock(data) } dataTask.resume() }

For this tutorial, we are not going to write tests for this class, but it’s doable using a third-party library like OHTTPStubs.

Writing Unit Tests and Writing Code to Make Them Pass

After finishing the design and writing the integration test, we will go through each object and write unit tests for every class, and then write code to make these tests pass.

MoviesListModel

MoviesListModel will use the network layer to fetch and parse data. In order to test MovieListModel , we need to stub network requests. So we need to implement a mock object for the network layer that will take the desired response as a parameter in initialization and return it inside a callback.

class NetworkLayerMock: Network { private let mockedData: [[String:String]] init(mockedData:[[String:String]]) { self.mockedData = mockedData } override func executeGETRequest(api:String, completionBlock:@escaping (Data?) -> Void) { let data = toJSONString(mockedData: self.mockedData) completionBlock(data) } func toJSONString(mockedData:[[String:String]]?) -> Data? { if let arr = mockedData { let dat = try? JSONSerialization.data(withJSONObject: arr, options: .prettyPrinted) return dat } return nil } }



I expect to fetch movies on MoviesListModel and return the movies in the callback. So the test should be like this:

func testFetchingMoviesFromNetworkLayer() { let networkLayer = NetworkLayerMock(mockedData: [["name":"name1", "rating": "12"], ["name":"name2", "rating": "123"]]) let movieslistModel = MoviesListModel(networkLayer: networkLayer) let moviesListModelDelegateMock = MoviesListModelDelegateMock() movieslistModel.delegate = moviesListModelDelegateMock movieslistModel.fetchMovies() XCTAssertTrue(moviesListModelDelegateMock.movies.count == 2, "Failed to return the expected count of movies") let firstMovie = moviesListModelDelegateMock.movies[0] let secondMovie = moviesListModelDelegateMock.movies[1] // Asserting on movies values XCTAssertTrue(firstMovie.name == "name1", "failed to parse first movie name") XCTAssertTrue(secondMovie.name == "name2", "failed to parse second movie name") XCTAssertTrue(firstMovie.rating == "12", "failed to parse first movie rating") XCTAssertTrue(secondMovie.rating == "123", "failed to parse second movie rating") }



MoviesListModelDelegateMock is a helper class that will help me to assert movie values.

class MoviesListModelDelegateMock: NSObject, MoviesListModelDelegate { public var movies:[Movie] = [] func didFetchMovies(success: Bool, movies: [Movie]) { self.movies = movies; } }

Finally, we are going to write the actual code. Let’s implement MoviesListModel to make its tests pass.

func fetchMovies() { self.networkLayer.executeGETRequest(api: "/Movies", completionBlock: { (data) in if let moviesData = data { let movies = self.parseMovies(data: moviesData) if let delegate = self.delegate { delegate.didFetchMovies(success: true, movies: movies) return } } if let delegate = self.delegate { delegate.didFetchMovies(success: false, movies: []) return } }) } func parseMovies(data:Data) -> [Movie] { do { var movies:[Movie] = [] if let jsonArray = try JSONSerialization.jsonObject(with: data, options : .allowFragments) as? [[String:String]] { for object in jsonArray { let movie = Movie() movie.name = object["name"] movie.rating = object["rating"] movies.append(movie) } return movies } else { return [] } } catch let error as NSError { print(error) } return [] }

We will call the network layer, parse the returned object, then return it through the delegate.

Let ’s run MovieListModelTests again.

Congratulations, your first test passed! ?

Note: According to the Single Responsibility Principle, MovieListModel is doing two jobs: fetching data from the network and parsing it. So we need to separate the parsing functionality into another class.

MovieParser

Once again, we will first create tests for MovieParser and watch them fail, then implement the parser. After finishing the refactor, you will need to run MovieListModelTests again to make sure that everything is working fine.

MoviesListPresenter

We will begin with writing tests inside MoviesListPresenterTests . First, we will need to create MoviesListModelMock to stub fetching movies so that we can easily test only MoviesListPresenter .

class MoviesListModelMock: MoviesListModel { private var mockedMovies: [Movie] = [] convenience init(mockedMovies:[Movie]) { self.init(networkLayer: Network()) self.mockedMovies = mockedMovies } override func fetchMovies() { if let delegate = self.delegate { delegate.didFetchMovies(success: false, movies: self.mockedMovies) return } } }

Next, we will pass the mock movies to MoviesListModel and pass it as a parameter to MoviesListPresenter . Then, assert on Presenter methods.

func testLoadingMovies() { let movie1 = Movie() movie1.name = "name1" movie1.rating = "99" let movie2 = Movie() movie2.name = "name2" movie1.rating = "100" let moviesListModelMock = MoviesListModelMock(mockedMovies: [movie1, movie2]) let moviesListPresenter = MoviesListPresenter(moviesListModel: moviesListModelMock) moviesListPresenter.fetchMovies() XCTAssertTrue(moviesListPresenter.movieName(index: 0) == "name1", "failed to fetch first movie name") XCTAssertTrue(moviesListPresenter.movieName(index: 1) == "name2", "failed to fetch second movie name") }

Now, we need to implement the MoviesListPresenter class to make the tests green.

Writing Code to Make Our Integration Test Pass

Now that we’ve written failed unit tests and made them pass, we can write our code to make the integration test pass.

We need to make an initializer that will take MoviesListModel as a parameter to allow Presenter to use it and make Presenter be its delegate.

init(moviesListModel:MoviesListModel) { self.moviesListModel = moviesListModel super.init() self.moviesListModel.delegate = self }



The presenter will fetch movies from the model.

public func fetchMovies () { self.moviesListModel.fetchMovies() }

ViewController needs to display data inside UIComponents and Presenter should not expose Model to the UI. So we need to create an abstraction layer to hide Model from our UI.

public func moviesCount() -> Int { return self.movies.count } func movieName(index:Int) -> String { let movie = self.movies[index] if let movieName = movie.name { return movieName } return "" } func movieRating(index:Int) -> String { let movie = self.movies[index] if let movieRating = movie.rating { return movieRating } return "" }

Then we can run all our unit tests and the integration test. Everything should pass. ✅

The final thing we need to do is make the end-to-end test pass.

Writing Code to Make Our End-to-End Test Pass

After implementing the UI, you will need to create an instance from MoviesListPresenter inside MoviesListViewController and implement its delegate.

override func viewDidLoad() { super.viewDidLoad() self.moviesListPresenter = self.moviesPresenter() self.moviesListPresenter?.delegate = self self.moviesListPresenter?.fetchMovies() /// Do any additional setup after loading the view./ } func moviesPresenter() -> MoviesListPresenter { let networkLayer = Network() let moviesModel = MoviesListModel(networkLayer: networkLayer) let moviesListPresenter = MoviesListPresenter(moviesListModel: moviesModel) return moviesListPresenter }

Then, run your end-to-end test. Everything should pass. ?

Conclusion

If you have multiple acceptance criteria, you will need to write more integration tests for each of them to complete your user story.

In this tutorial, you learned how to implement a complete user story with solid tests without the need for backend integration. To make sure you are integrated correctly with the cloud, you will need a copy of the end-to-end test to actually run on a testing account.

Now thanks to TDD, after you develop more features in this project, if one of the above tests fail, you’ll have a quick hint about which part in the code has a problem.

You can find the complete project here: Sample TDD Project.