When I started with Unit Testing I found some tutorials and it was very easy to understand how it works. But the problem was not how to test but what to test and how to build an architecture to let my code be fully testable. I kept searching the web and didn’t found the answer. So now I will try to give a short answer for what to test and how to build architecture.
It is always a big question and it very depends on how much time you have for the feature you developing. Ideally, we what to cover much code as possible but first of all we should pay attention to places where:
Those are a few places when I think it necessary to create tests but in a long perspective test everything you can.
When you building the architecture you need to separate your every module and use dependency injection.
Let’s say we have a cloud music app, listening to some albums is a paid feature, so each time user wants to listen and doesn’t have a required tier we should present an upsell with a proposition to purchase this tier. It is very important to avoid bugs here so let’s test it.
To demonstrate this I will try to use very simple code with the small architecture as much as I can just to let you understand the concept.
I will create a project with the following dependencies:
This is how my Album object looks like:
struct Album: Codable { let isAllowed: Bool let requiredPackageName: String let requiredPackageId: String }
This is how my AlbumProvider looks like:
protocol AlbumProviderProtocol { func getAlbum(completion: @escaping (Result<Album,AlbumError>)->Void) } enum AlbumError: Error { case apiError(Error), notAllowed } class AlbumProvider { let repository: RepositoryProtocol init(repository: RepositoryProtocol) { self.repository = repository } } extension AlbumProvider: AlbumProviderProtocol { func getAlbum(completion: @escaping (Result<Album,AlbumError>)->Void) { let requestConfiguration = RequestConfiguration(url: URL(string: "http://www.adamofsky.com/album.json")!, httpMethod: .get) repository.request(requestConfiguration) { (result: Result<Album,Error>) in switch result { case .success(let album): guard album.isAllowed == true else { completion(.failure(.notAllowed)) return } completion(.success(album)) case .failure(let error): completion(.failure(.apiError(error))) } } } }
As you can see it must be initialized with Repository and implement AlbumProviderProtocol with a function that just requests an album from the repository and returns the album or an error in a case where the user does not have permission and needs to purchase the required tier or any other error.
This is how my Repository looks like:
protocol RepositoryProtocol { func request<T:Codable>(_ requestValue: RequestConfiguration, completion: @escaping (Result<T, Error>)->Void) } struct RequestConfiguration { var url: URL var httpMethod: RequestManager.HTTPMethod } class Repository: RepositoryProtocol { private let requestManager = RequestManager() func request<T:Codable>(_ requestConfiguration: RequestConfiguration, completion: @escaping (Result<T, Error>)->Void) { requestManager.request(with: requestConfiguration.url, httpMethod: requestConfiguration.httpMethod) { (result) in switch result { case .success(let data): let decoder = JSONDecoder() do { let object = try decoder.decode(T.self, from: data) completion(.success(object)) } catch let error { completion(.failure(error)) } case .failure(let error): completion(.failure(error)) } } } }
If you don’t familiar with a repository pattern I recommend you to research it, but for now, as you can see in our case it just provides communication between the provider and request manager and returns a decoded object if the API call succeeded.
Well so this is our simple flow: view model requests the album from the provider, provider requests from the repository, and repository requests from the request manager and returns the result. The provider receives the final result and the view model by this result decides to let access to the album or not.
Let’s test our provider.
Here we should create our test.
Since I want to test the provider I need to initialize him here, for initializing I need a repository, but I do not want that this repository will request an API call I want the repository to return me some mock data.
For this, I create a class AlbumRepositoryMock and it looks like this:
class AlbumRepositoryMock: RepositoryProtocol { func request<T:Codable>(_ requestConfiguration: RequestConfiguration, completion: @escaping (Result<T, Error>)->Void) { let album = Album(isAllowed: false, requiredPackageName: "Professional", requiredPackageId: "pro") completion(.success(album as! T)) } }
Here the request function simulates the response I want, the response I want to be handled correctly by my provider.
So my testHandleNotAllowedAlbum function will look like this:
func testHandleNotAllowedAlbum() throws { let repository = AlbumRepositoryMock() let albumProvider = AlbumProvider(repository: repository) albumProvider.getAlbum(completion: { (result) in switch result { case .success(_): XCTAssert(false) case .failure(let error): switch error { case .apiError(_): XCTAssert(false) case .notAllowed: XCTAssert(true) } } }) }
As you can see our test will succeed only when our code will reach the .notAllowed case, otherwise will fail.
You are welcome to clap if you like it.
4
1 Comment
nice tutorial !
i just started with unit tests and in the phase that i should bit refactor and this is exactly what i search
great work man !