Trikalabs
  • Home
  • Best online TDD videos
  • Book Suggestions
  • About Me
  • Contact
Trikalabs
No Result
View All Result

Unit Test Result type and closure

by fragi
July 19, 2019
in Unit Testing
Share on FacebookShare on Twitter

In this article, we will see how to test Result types and closures. To keep it simple, we will use only one ViewController that calls a MovieLoader object to fetch movies. The MovieLoader will then return a list of movies if the request is successful or an error if the request fails.

Let’s see how the initial code looks:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class ViewController: UIViewController {
    var moviesLoader: MoviesLoaderType = MoviesLoader() // This has to be injected. In this tutorial we focus only on how to test Result type and closures.
 
    private(set) var movies : [Movie] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //Fetching the data inside the ViewController is not recommended. But in this tutorial we focus only on how to test Result type and closures.
        moviesLoader.fetch {[weak self] result in
            switch result {
            case .success(let movies):
                self?.movies = movies
            case .failure(let error):
                print(error.localizedDescription)
            }
        }
    }
 
}
 
//MoviesLoader.swift
enum LoadingError: Error {
    case noInternet
}
 
protocol MoviesLoaderType {
    func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ())
}
 
class MoviesLoader: MoviesLoaderType {
    
    func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ()) {
        
        // Some asynchronous code ......... that fetch movies
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            completion(.success([Movie(name: "Avengers"), Movie(name: "Star Wars")]))
        }
    }
}
 
//Movie.swift
struct Movie: Equatable {
    let name: String
}

You can find the start project in this link.

Now let’s see how we can unit test this functionality. We firstly want to test that the viewController, when the method viewDidLoad is called, asks the MovieLoader to fetch movies. We have a case that one object interacts with  another by calling a method, and we want to test this interaction. In these cases, we can create a spy so we intercept the method call. Let’s write a test method for this scenario and let’s create the spy class.

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ViewControllerTests: XCTestCase {
 
    func test_viewDidLoad_callsFetchOnMoviesFetcher() {
        let sut = ViewController()
        let spyMoviesLoader = SpyMoviesLoader()
        sut.moviesLoader = spyMoviesLoader
        
        _ = sut.view
        
        XCTAssertEqual(1, spyMoviesLoader.numberOfTimesCalledFetch)
    }
 
}
 
extension ViewControllerTests {
    
    class SpyMoviesLoader: MoviesLoader {
        
        private(set) var numberOfTimesCalledFetch = 0
        override func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ()) {
            numberOfTimesCalledFetch += 1
        }
    }
 
}

 Press CMD + U to run the test. The test pass. To be sure that we are testing the right thing, we should go to the viewController and comment out the functionality that calls the MovieLoader. Then we can run again the test, so we can see it fail. After seeing a test failure we can bring the code back to the previous state. A better approach would have been to develop the code from the beginning with Test Driven Development.

Let’s focus now on the callback functionality of the fetch method. There are two scenarios that we want to test. Firstly let’s test the success scenario. All we want to test here is that when the fetch method return successfully a list of movies we assign this list to array movies property  in our ViewController:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func test_fetch_success_setsMovies() {
        let sut = ViewController()
        let stubMoviesLoader = StubMoviesLoader()
        let expectedMovies = [Movie(name: "Avengers"), Movie(name: "Star Wars")]
        stubMoviesLoader.stubReturnMovies(movies: expectedMovies)
        sut.moviesLoader = stubMoviesLoader
        
        _ = sut.view
        
        XCTAssertEqual(expectedMovies, sut.movies)
    }
 
extension ViewControllerTests {
    
    class SpyMoviesLoader: MoviesLoader {
        
        private(set) var numberOfTimesCalledFetch = 0
        override func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ()) {
            numberOfTimesCalledFetch += 1
        }
    }
    
    class StubMoviesLoader: MoviesLoader {
        private var movies : [Movie] = []
        
        func stubReturnMovies(movies: [Movie]) {
            self.movies = movies
        }
        
        override func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ()) {
            completion(.success(movies))
        }
    }
 
}

Run the tests. The tests pass. Here we created a Stub to help us take control of the dependency. In this way, we can test the success scenario without the need to call the actual service. This makes the test run fast and independent. Also there are cases that we can call the service either because is expensive to call it, or because simple the service doesn’t exist yet. 

Last functionality we want to test is the failure scenario. Similar to the previous test we will use the same stub but we will modify it, so to be able to support both cases:

Swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func test_fetch_failure_moviesArrayIsEmpty() {
        let sut = ViewController()
        let stubMoviesLoader = StubMoviesLoader()
        stubMoviesLoader.stubReturnFailureNoInternet()
        sut.moviesLoader = stubMoviesLoader
 
        _ = sut.view
 
        XCTAssertEqual([], sut.movies)
    }
 
class StubMoviesLoader: MoviesLoader {
        private var movies : [Movie] = []
        private var shouldShowLoadingError: LoadingError?
        
        func stubReturnMovies(movies: [Movie]) {
            self.movies = movies
        }
 
        func stubReturnFailureNoInternet() {
            self.shouldShowLoadingError = .noInternet
        }
        
        override func fetch(completion: @escaping (Result<[Movie], LoadingError>) -> ()) {
            if let loadingError =  self.shouldShowLoadingError {
                completion(.failure(loadingError))
            } else {
                completion(.success(movies))
            }
        }
    }

Run the tests. All test pass.

 

 

fragi

fragi

Related Posts

What is TDD?
Unit Testing

Write better Unit tests with XCTUnwrap

January 27, 2022

Testing optionals can require boilerplate code to unwrap it. It can also affect the readability of the test. Let's look...

Test static method on collaborator
Unit Testing

Test static method on collaborator

May 13, 2021

One of the main issues we are facing when we try to make an old piece of code testable is...

Dependency injection – UIViewController in Storyboard (iOS 13)
Unit Testing

Dependency injection – UIViewController in Storyboard (iOS 13)

May 13, 2021

Finally Apple (iOS 13 +) has added constructor dependency injection to Storyboards! No excuses anymore for not using DI properly...

Unit testing code with DispatchQueue
Unit Testing

Unit testing code with DispatchQueue

May 13, 2021

One way of testing code with DispatchQueue is by using expectations (https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations) but in most of the cases it's more...

Unit testing UIViewController LifeCycle
Unit Testing

Unit testing UIViewController LifeCycle

May 13, 2021

Unit testing UIViewController life cycle events are not a straightforward process. Let's have a look on the following view controller:...

Dependency Injection – UIViewController with a Nib file
Unit Testing

Dependency Injection – UIViewController with a Nib file

May 13, 2021

UIViewControllers are among the most used UIKit objects. As the name implies UIViewControllers are controller objects and their purpose should...

Next Post
Unit Test NotificationCenter

Unit Test NotificationCenter

Unit Test Post Notification

Unit Test Post Notification

AWS: Integrate User Pools on iOS Apps

AWS: Integrate User Pools on iOS Apps

  • Advertise
  • Privacy & Policy
  • Contact

© 2019 Trikalabs Ltd. All rights reserved.

No Result
View All Result
  • Home
  • About Me
  • A curated list with the best free online TDD videos
  • Book Suggestions
  • Pinner Code Club

© 2019 Trikalabs Ltd. All rights reserved.