This document discusses unit testing and why it is important for software developers. It provides an overview of unit testing, including what unit testing is, why we should do it, how the testing process works. It then gives a example of unit testing a sample photo gallery app, demonstrating how to test models, services, and view models. It also covers best practices for unit testing like mocking dependencies and using stubs to generate test data.
2. Vodafone Proprietary classified as C2 - Internal
2
Hello!
I am Yahya Saddiq, hands-on iOS Lead, _VOIS
at Vodafone.
A father of two, fitness enthusiast and coding
is my passion.
You can find me on LinkedIn.
3. Vodafone Proprietary classified as C2 - Internal
The Beginning
2 February 20203
• Unit Testing is really important to anyone who is getting started in software
development, whether you’ve just discovered it, self teaching or going to school.
• Even if you’ve got yourself into new job, may be you haven’t been exposed to it yet
cause maybe your organization doesn’t do that.
4. Vodafone Proprietary classified as C2 - Internal
The Beginning
2 February 20204
• It’s really important to learn it from the beginning cause you never know at what
point at your career or job interview ask you about unit testing and currently
that’s inevitable.
• It’s really important to have the conceptual ideas down your head and being able
to explain it to someone.
Unit Testing is Awesome
😎
7. Vodafone Proprietary classified as C2 - Internal
What is Unit Testing?
2 February 20207
• Unit testing helps you to find failures and embarrassing mistakes from being
introduced into your production app
• Helps you have a software that actually works reliably.
• Unit Testing is a White Box testing
8. Vodafone Proprietary classified as C2 - Internal
What is Unit Testing?
2 February 20208
• The purpose is to validate that each unit of the software code performs as
expected.
• Unit Testing is done during the development of an application by you (developers).
• Unit Tests isolate a section of code and verify its correctness.
• A unit may be an individual
– Function
– Method
– Procedure
– Module
– or Object.
9. Vodafone Proprietary classified as C2 - Internal
Why We Should Do it?
2 February 20209
• We write unit tests, because we want our code to work and to keep it working.
• Unit tests are a great way to write better code.
• Tests help you find most of the bugs early on in the process.
• More importantly, writing code in a test-based development mindset helps you write
modular code that’s easy to maintain.
Easy to test code,
is easy to maintain and debug code.
10. Vodafone Proprietary classified as C2 - Internal
Why We Should Do it?
2 February 202010
When it comes to testing, there’s good news, and bad news.
• The bad news is that there can be disadvantages to unit tests.
– More code
– More to maintain
– No silver bullet
– Takes longer
11. Vodafone Proprietary classified as C2 - Internal
Why We Should Do it?
2 February 202011
Although there is no silver bullet, there is a silver lining.
• Testing has the following advantages
– Protection against regression
– Less coupled code
– Confidence
– Quick feedback
– Focus
– Refactoring
– Executable documentation
Maximizes laziness
😴
13. Vodafone Proprietary classified as C2 - Internal
The 5 characteristics of Unit Testing
2 February 202013
•Fast.
•Independent/Isolated.
•Repeatable.
•Self-validating.
•Timely
Following the FIRST principles,
keeps your tests clear and helpful.
14. Vodafone Proprietary classified as C2 - Internal
A simple gallery app
2 February 202014
We are going to test a simple app, in which:
– The app fetches popular photos from 500px API and lists photos in
a UITableView.
– Each cell in the table view shows a title, a description and the
created date of a photo.
– Users are not allowed to click photos which are not labeled
for_sale.
15. Vodafone Proprietary classified as C2 - Internal
A simple gallery app
2 February 202015
• The first screen in the app is called PhotoListViewController,
and this is its the data flow.
• We are going to test:
1. Does APIService work and return valid photos?
2. Does photoListCellViewModel correctly created from a photo
object?
3. Does PhotoListViewModel fetch data from the APIService?
4. Does PhotoListViewModel display an error if the request failed?
5. Does PhotoListViewModel allow the segue if the user presses a “for
sale” photo?
16. Vodafone Proprietary classified as C2 - Internal
The First Test Case
2 February 202016
Does photoListCellViewModel correctly created from a photo object?
• Create a test class for PhotoListViewModel.
• Let’s give the unit tests an access to the internal types and functions in
MVVMPlayground.
@testable import MVVMPlayground.
• Create a placeholder for a PhotoListViewModel, which is the System Under Test, or
the object this test case class is concerned with testing.
var sut: PhotoListViewModel!
17. Vodafone Proprietary classified as C2 - Internal
Does photoListCellViewModel correctly
created from a photo object?
2 February 202017
• Create a PhotoListViewModel object at the class level, so all the tests in this test
class can access the SUT object’s properties and methods.
super.setUp()
sut = PhotoListViewModel()
• Before we forget, release the SUT object in tearDown().
sut = nil
super.tearDown()
• Setup and teardown prepare initial state before tests run and perform cleanup after
tests complete.
18. Vodafone Proprietary classified as C2 - Internal
2 February 202018
// A test method’s name always begins with test, followed by a description of what it tests.
func test_cell_view_model() {
// It’s good practice to format the test into given, when and then sections
// Given: Here, you set up any values needed.
// In this example, today’s date and a dummy photo object are created.
let today = Date()
let photo = Photo(id: 1,
name: "Name",
description: "desc",
created_at: today,
image_url: "url",
for_sale: true,
camera: "camera")
// When: In this section, you’ll execute the code being tested: Call createCellViewModel(photo:).
let cellViewModel = sut!.createCellViewModel(photo: photo)
// Then: This is the section where you’ll assert the result you expect with a message that prints if the test fails.
XCTAssertEqual(cellViewModel.descText, "(photo.camera!) - (photo.description!)")
}
19. Vodafone Proprietary classified as C2 - Internal
Debugging a Test
2 February 202019
• To cause a test failure, let’s change desc join character to +
• In the Breakpoint navigator, add a Test Failure Breakpoint.
– It will stop the test run when a test method posts a failure assertion.
• Run the test, and it should stop at the XCTAssertEqual line with a test failure.
20. Vodafone Proprietary classified as C2 - Internal
Test Asynchronous Operations
2 February 202020
Does APIService work and return valid photos?
• URLSession methods are asynchronous:
– They return right away, but don’t finish running until later.
– use XCTestExpectation to make your test wait for the asynchronous
operation to complete.
21. Vodafone Proprietary classified as C2 - Internal
Does APIService work and return valid photos?
2 February 202021
@testable import MVVMPlayground
class APIServiceTests: XCTestCase {
var sut: APIService!
override func setUp() {
super.setUp()
sut = APIService()
}
override func tearDown() {
sut = nil
super.tearDown()
}
}
22. Vodafone Proprietary classified as C2 - Internal
Does APIService work and return valid photos?
2 February 202022
func test_fetch_popular_photos() {
// Given: Returns an XCTestExpectation object, stored in promise.
// The description parameter describes what you expect to happen.
let promise = XCTestExpectation(description: "Fetch popular photos
completed")
// When
sut.fetchPopularPhoto(complete: { (photos, error) in
// Then
guard error == nil,
let photos = photos else {
if let errorDesc = error?.rawValue {
XCTFail("Error: (errorDesc)")
}
return
}
XCTAssertEqual(photos.count, 20)
for photo in photos {
XCTAssertNotNil(photo.id)
}
// Call this in the success condition closure of the
asynchronous method’s completion handler to flag that the expectation
has been met.
promise.fulfill()
})
// Keeps the test running until all expectations are fulfilled, or
the timeout interval ends, whichever happens first.
wait(for: [promise], timeout: 3.1)
}
23. Vodafone Proprietary classified as C2 - Internal
Fail Fast
2 February 202023
• Failure hurts, but it doesn’t have to
take forever.
• Let’s update the file name to test a
failure and run the test.
• Now, Let’s improve it and make the
test fail faster by changing the
assumption.
• Run the test. It should now take about
a second to fail.
func test_fetch_popular_photos_completes() {
// Given
let promise = XCTestExpectation(description: "Fetch popular
photos completed")
var responseError: Error?
var responsePhotos: [Photo]?
// When
sut.fetchPopularPhoto(complete: { (photos, error) in
responseError = error
responsePhotos = photos
promise.fulfill()
})
wait(for: [promise], timeout: 3.1)
// Then
XCTAssertNil(responseError)
XCTAssertEqual(responsePhotos?.count, 20)
}
24. Vodafone Proprietary classified as C2 - Internal
Faking Objects and Interactions
2 February 202024
• Asynchronous tests give you confidence that your code generates correct input to
an asynchronous API.
• Most apps interact with system or library objects — objects you don’t control such
as Alamofire, NotificationCenter or UserDefaults. Tests that interact with these
objects can be slow and unrepeatable, violating two of the FIRST principles.
• Instead, you can fake the interactions by getting input from stubs or by
updating mock objects.
• We should mock all mentioned Black Boxes because unit testing is White Box
testing.
25. Vodafone Proprietary classified as C2 - Internal
Mocking - Fake Update to Mock Object
2 February 202025
Does PhotoListViewModel fetch data from the APIService?
• We use a technique named Dependency Injection (DI) to design
our PhotoListViewModel.
• The property apiService is a dependency of the PhotoListViewModel.
• In the production environment we assign an APIService object, which connects to a
real server, to the PhotoListViewModel.
• In the test environment, we inject a mock APIService object instead.
26. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel fetch data from
the APIService?
2 February 202026
@testable import MVVMPlayground
// The mock APIService (APIServiceMock) object doesn’t connect to the real server,
// it’s an object designed only for the test.
// Both APIService and APIServiceMock conform to APIServiceProtocol,
// so that we are able to inject different dependency in different situation.
class APIServiceMock: APIServiceProtocol {
var isFetchPopularPhotoCalled = false
func fetchPopularPhoto(complete: @escaping ([Photo]?, APIError?) -> ()) {
isFetchPopularPhotoCalled = true
}
}
27. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel fetch data from
the APIService?
2 February 202027
• Create an instance of the mock in PhotoListViewModelTests
var apiServiceMock: APIServiceMock!
apiServiceMock = APIServiceMock()
• Inject apiServiceMock to PhotoListViewModel
sut = PhotoListViewModel(apiService: apiServiceMock)
• Add the test case
func test_fetch_photo() {
// When
sut.initFetch()
// Then
XCTAssert(apiServiceMock.isFetchPopularPhotoCalled)
}
28. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel fetch data from
the APIService?
2 February 202028
• In addition to the behavior test, we also want to see if
the PhotoListViewModel correctly handles networking states.
• By using the DI technique, we are able to simulate the success and failure
networking states by changing the response of the MockAPIService.
• Let’s take a look at the PhotoListViewModel.initFetch():
29. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel display an error if the request
failed?
2 February 202029
class APIServiceMock: APIServiceProtocol {
var isFetchPopularPhotoCalled = false
var completeClosure: (([Photo]?, APIError?) -> ())!
func fetchPopularPhoto(complete: @escaping ([Photo]?, APIError?) -> ()) {
isFetchPopularPhotoCalled = true
// the callback closure is saved to the completeClosure for the later use.
completeClosure = complete
}
// the function call is finished but the escaping closure is still pending.
//The closure won’t be triggered until the fetchSuccess() or the fetchFail(error:) is called.
// Finally, when we call the fetchSuccess() or the fetchFail(error:),
// the PhotoListViewModel receives the response data and continue to finish its jobs.
func fetchSuccess() {
completeClosure([Photo](), nil)
}
func fetchFail(error: APIError?) {
completeClosure(nil, error)
}
}
30. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel display an error if the request
failed?
2 February 202030
func test_fetch_photo_fail() {
// Given
let error = APIError.permissionDenied
// When
sut.initFetch()
// here’s a trick: we ask the mock to fail the request by calling the fetchFail(error:) function.
apiServiceMock.fetchFail(error: error)
// Then
// assert the alert message of the PhotoListViewModel to see if it handles the error correctly.
XCTAssertEqual(sut.alertMessage, error.rawValue)
}
31. Vodafone Proprietary classified as C2 - Internal
Stubbing - Fake Input From Stub
2 February 202031
Does PhotoListViewModel allow the segue if the user presses a “for sale” photo?
• The ViewModel receives the user interaction and changes the presentation with
respect to the interaction.
• In MVVM, the user interactions are abstracted into a set of methods such
as userPressed(), userSwipe(), etc.
• Therefore, testing the user interactions is straightforward: we call a certain method
and assert the corresponding property of the ViewModel.
32. Vodafone Proprietary classified as C2 - Internal
Does PhotoListViewModel allow the segue if the user
presses a “for sale” photo?
2 February 202032
func test_user_press_for_sale_item() {
// Given
let indexPath = IndexPath(row: 0, section: 0)
// When
sut.userPressed(at: indexPath)
// Then
XCTAssertTrue(sut.isAllowSegue)
}
33. Vodafone Proprietary classified as C2 - Internal
2 February 202033
class APIServiceMock: APIServiceProtocol {
var isFetchPopularPhotoCalled = false
// We create an array, completePhotos, to save the stubs.
// And those stubs will be returns to the PhotoListViewModel once the PhotoListViewModel calls
fetchPopularPhoto(complete:).
var completePhotos: [Photo] = [Photo]()
var completeClosure: (([Photo]?, APIError?) -> ())!
func fetchPopularPhoto(complete: @escaping ([Photo]?, APIError?) -> ()) {
isFetchPopularPhotoCalled = true
completeClosure = complete
}
func fetchSuccess() {
completeClosure(completePhotos, nil)
}
func fetchFail(error: APIError?) {
completeClosure(nil, error)
}
}
Does PhotoListViewModel allow the segue if the user
presses a “for sale” photo?
34. Vodafone Proprietary classified as C2 - Internal
2 February 202034
func test_user_press_for_sale_item() {
// Given
let indexPath = IndexPath(row: 0, section: 0)
// The StubGenerator().stubPhotos() generates couple of photo objects.
// Then we assign the photo objects to the mockAPIService.completePhotos.
apiServiceMock.completePhotos = StubGenerator().stubPhotos()
sut.initFetch()
// When the request finished (mockAPIService.fetchSuccess() is called),
// the PhotoListViewModel will receive those photo stubs via the callback closure.
apiServiceMock.fetchSuccess()
// When
sut.userPressed(at: indexPath)
// Then
XCTAssertTrue(sut.isAllowSegue)
}
Does PhotoListViewModel allow the segue if the user
presses a “for sale” photo?
35. Vodafone Proprietary classified as C2 - Internal
Code coverage
2 February 202035
• The code coverage tool tells you what app code is actually being run by your tests,
so you know what parts of the app code aren’t (yet) tested.
• To enable code coverage, edit the scheme’s Test action and check the Gather
coverage for check box under the Options tab.
• Run all tests (Command-U), then open the Report navigator (Command-9).
Select Coverage under the top item in that list:
• Research shows that 80% is best cost-efficient code coverage.
36. Vodafone Proprietary classified as C2 - Internal
Do's and Don'ts
2 February 202036
• Verify One Concern Per Test Method
– A concern is a single end result: a return value or change to app state.
– if the first assert fails, does it matter what happens to the next one?
– If yes, you must split the test into smaller ones.
• Control Side-Effects
– When we change global app data from our test, this indirectly affects the whole test suite.
– To prevent this from happening you must cleanup before and after each test
• Do Not Leak Test Code into Production
– Firstly, do not weaken encapsulation for the purpose of testing.
– It’s either a flaw in the app design or you might be testing too much.
– Secondly, do not put logic into production code, only to support testing.
– Changes in your test code should not affect the production.
37. Vodafone Proprietary classified as C2 - Internal
2 February 202037
Recap
• We used Unit Test to check:
– If APIService works and returns valid photos.
– If photoListCellViewModel correctly created from a photo object.
– If the SUT correctly interacts with the APIService.
– If the SUT handles the error state correctly.
– If the SUT handles the user interaction correctly.
• Now, you are able to use the knowledge you learned to write more test cases:
– The loading animation should start when the network request starts.
– The loading animation should stop when the request completes.
– The table view should render correctly.
– It should display error message when user press on the photo that is not for sale.
Editor's Notes
Unit Tests. These should count for the majority of tests you have for your codebase. They tests the smallest unit of code possible. Normally, only test a single method each.
Integration Tests to verify the integration of different parts of separate components of a software system together. This can be integration with a Database, with a Framework, with third party external software systems.
At the top is the end-to-end tests which are tests that verifies the end to end workflows of your codebase. They tests the system from the user-action entry point right to the end of the system down to the database level. However, these are typically black-box tests.
More code: In projects with high test coverage it’s possible to have more test code than functional code.
More to maintain: When there is more code, there is more to maintain.
No silver bullet: Unit tests don’t (and can’t) ensure that your code is free of bugs.
Takes longer: Writing tests takes time — time you could spend watching Netflix.
Regression defects: are defects that are introduced when a change is made to the application. It is common for testers to not only test their new feature but also features that existed beforehand in order to verify that previously implemented features still function as expected.
Less Coupled: Writing tests for your code will naturally decouple your code, because it would be more difficult to test otherwise.
Confidence: You can demonstrate that your code works.
Quick feedback: You can use unit tests to quickly validate code that is buried many layers deep in your app navigation — things that are cumbersome to test manually.
Focus: Writing tests for micro features keep you focused on the small details.
How this process works in real software world?
It works through continuous integration, every time someone commit their code to your code repository you fire all your tests and if even one test fail the entire team get an email with the failure, the cause and the person who should fix it.
While both human code reviews and pair programming can be effective in monitoring coding standards, they do not scale as well as automated tools. Not only do tools contain hundreds of rules (that are usually customizable), they can be run frequently and usually without intervention.
In a CI environment, a code analysis tool can be run any time a change is made to the repository.
Code analysis tool like SonarQube can report complexity metrics like cyclomatic complexity, long methods, long classes, unnecessary verbose code, a poorly named methods/classes/variables.
By continuously monitoring and auditing code, your team can stay on track with architectural and coding guidelines.
Fast. It is not uncommon for mature projects to have thousands of unit tests. Unit tests should take very little time to run. Milliseconds.
Independent/Isolated. Unit tests are standalone, can be run in isolation, and have no dependencies on any outside factors such as a file system or database.
Repeatable. Running a unit test should be consistent with its results, that is, it always returns the same result if you do not change anything in between runs. External data providers or concurrency issues could cause intermittent failures.
Self-validating: Tests should be fully automated. The output should be either “pass” or “fail”, rather than rely on a programmer’s interpretation of a log file.
Timely. A unit test should not take a disproportionately long time to write compared to the code being tested. If you find testing the code taking a large amount of time compared to writing the code, consider a design that is more testable.
Run the app
The APIService takes the responsibility of the network layer, such as setting up the URL, sending the request, etc.
The PhotoListViewModel asks the APIService for photo objects and provides the presentational interfaces for the PhotoListViewController.
The PhotoListViewController is a simple View, rendering the visible element according to the data presented by the PhotoListViewModel.
why super.teardown last?
This method is called last in case the current class is relying on something that might be nulled out or cleaned up in the superclass.
Run the test by clicking the diamond icon in the gutter or in the Test navigator. This will build and run the app, and the diamond icon will change to a green check mark!
Note: To see a full list of XCTestAssertions, go to Apple’s Assertions Listed by Category.
It fails, but it takes the full timeout interval!
This is because you assumed the request would always succeed, and that’s where you called promise.fulfill(). Since the request failed, it finished only when the timeout expired.
We don’t create an instance of APIService inside the viewModel (that’s tightly coupled)
We pass APIService to viewModel in the class constructor (that’s DI)
If you’re not familiar with the idea of dependency injection, feel free to check my talk MVVM with DI on youtube
This is a simple test case.
The code snippet shows that when the PhotoListViewModel fetches data, we check if it called the fetchPopularPhoto(complete:) method.
By using this technique, we are able to check if PhotoListViewModel calls specified methods for the dependency injection. In other words, we successfully test the behavior of our ViewModel.
explain changes first
Now the APIServiceMock is able to simulate any kind of asynchronous request.
For example, we are able to assert the state of the ViewModel: state should be loading before fetchSuccess() is called.
This is a powerful technique because it mimics an async call, and it could be done immediately.
Run the test
Now the APIServiceMock is able to simulate any kind of asynchronous request.
For example, we are able to assert the state of the ViewModel: state should be loading before fetchSuccess() is called.
This is a powerful technique because it mimics an async call, and it could be done immediately.
Run the test
This test case describes a user who presses on the first cell, and we are going to see if the segue is allowed.
The problem is, we haven’t fetched any photo yet and the sut is currently in the empty state.
So if we trigger the sut.userPressed(at:) at this stage, the out of bounds exception will be raised.
We need to grab some data first!
explain changes first
Explain changes first
Run the test
It should succeed pretty quickly because there isn’t any real network connection!
With the help of those stubs, we are able to assert a certain action such as user presses on a specific IndexPath and so on.