How to write your first Unit Test
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
“How do I write my first Unit Test in Swift?” 🤔
If you’re new to Swift or iOS, you’re bound to ask yourself this question at some point!
So I made this article to guide you through that process in record time ⚡️
So let's get started!
I've implemented a simple ViewModel
that allows me to store a list of people
along with a list of filters
, and then only display the people that satisfy all of these filters:
class PeopleViewModel {
typealias PersonFilter = (Person) -> Bool
var people: [Person] = []
var filters: [PersonFilter] = []
init(people: [Person], filters: [PersonFilter] = []) {
self.people = people
self.filters = filters
}
var peopleMatchingFilters: [Person] {
var filteredPeople = people
for filter in filters {
filteredPeople = filteredPeople.filter(filter)
}
return filteredPeople
}
}
And now I want to write some tests to make sure that my ViewModel
does behave like I expect it to.
For this I'm going to move into my testing target.
(If you don't already have a testing target in your project, you can create one by clicking on this "+" icon right here, and then adding a new Unit Testing Bundle to your project)
You can notice that the Swift file that will contain my testing code looks a little bit different than the ones I have in my app:
import XCTest
@testable import YourFirstUnitTest
final class PeopleViewModelTests: XCTestCase {
}
it imports
XCTest
, which is Xcode's testing frameworkit does an
@testable import
ofMyApp
: this is important because this@testable import
will allow me to access the properties of myViewModel
that have an internal visibility and would otherwise be inaccessible outsideMyApp
finally, it defines a subclass of
XCTestCase
, which is the base class we need to use to write our tests
But before we start writing our first test, we're first going to add a static constant that will hold the data we're going to use in all our tests:
import XCTest
@testable import YourFirstUnitTest
final class PeopleViewModelTests: XCTestCase {
static let people = [
Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true),
]
}
And now we're all set to implement our first test!
To implement a test, we simply start by defining a new method, whose name begins with "test" and then describes the behavior we're testing.
For this first test, I want to test that my ViewModel behaves correctly when I give it a filter for the property firstName
, so I write the name of the test accordingly:
func testFirstNameFilter() {
}
Then, I'm going to structure the body of my test into three parts: Given, When and Then:
func testFirstNameFilter() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [{ $0.firstName == "Alex" }]
// Then
let expectedResult = [
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
]
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
In the "Given" part, I set up the context, meaning I create my ViewModel
with its initial data
In the "When" part, I perform the action I want to test: here I set up my filter
.
Finally, in the "Then" part, I check that my ViewModel
has behaved as I expected, by comparing the result it gives with my expected result
And that’s it, I can now run my test to make sure that my ViewModel
is indeed behaving as I expect it to 👌
Of course, one test is not enough to cover all the possible situations, so let’s write a few more tests!
Using the same approach than for the first test, I’m now going to write a test for the case where I set multiple filters:
func testMultipleFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [
{ $0.firstName == "Alex" },
{ $0.hasDriverLicense == false },
{ $0.isAmerican == true },
]
// Then
let expectedResult = [
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
]
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
Then, I’m going to write a couple more tests that will cover the edge cases, because it’s often there that you find some bugs!
First, I’ll write a test for the case where a combination of filters produces no result:
func testNoResultFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [
{ $0.firstName == "Alex" },
{ $0.hasDriverLicense == false },
{ $0.isAmerican == false },
]
// Then
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, [])
}
Then I’ll write another test for the case where I set no filters:
func testNoFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = []
// Then
let expectedResult = PeopleViewModelTests.people
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
To finish, I’m going to run all the tests to make sure that my ViewModel passes all of them:
And that’s it: we’ve implemented our first unit tests 🥳
As we’ve seen, the idea behind a unit test is pretty straightforward: we simply write code that asserts that under a given situation, our code does behave like we expect it to behave!
That’s all for this article, I hope you’ve enjoyed it!
You’ve enjoyed this article and you want to support my content creation?
You can leave me a tip ☺️ 👇
Here’s the full code if you want to experiment with it!
// Person.swift
import Foundation
struct Person: Equatable {
let firstName: String
let lastName: String
let age: Int
let hasDriverLicense: Bool
let isAmerican: Bool
}
// PeopleViewModel.swift
import Foundation
class PeopleViewModel {
typealias PersonFilter = (Person) -> Bool
var people: [Person] = []
var filters: [PersonFilter] = []
init(people: [Person], filters: [PersonFilter] = []) {
self.people = people
self.filters = filters
}
var peopleMatchingFilters: [Person] {
var filteredPeople = people
for filter in filters {
filteredPeople = filteredPeople.filter(filter)
}
return filteredPeople
}
}
// PeopleViewModelTests.swift
import XCTest
@testable import YourFirstUnitTest
final class PeopleViewModelTests: XCTestCase {
static let people = [
Person(firstName: "Charlie", lastName: "Webb", age: 10, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Charles", lastName: "Webb", age: 45, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
Person(firstName: "John", lastName: "Webb", age: 28, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Webb", lastName: "Elexson", age: 30, hasDriverLicense: true, isAmerican: true),
]
func testFirstNameFilter() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [{ $0.firstName == "Alex" }]
// Then
let expectedResult = [
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Zunino", age: 34, hasDriverLicense: true, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
]
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
func testMultipleFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [
{ $0.firstName == "Alex" },
{ $0.hasDriverLicense == false },
{ $0.isAmerican == true },
]
// Then
let expectedResult = [
Person(firstName: "Alex", lastName: "Elexson", age: 22, hasDriverLicense: false, isAmerican: true),
Person(firstName: "Alex", lastName: "Alexson", age: 8, hasDriverLicense: false, isAmerican: true),
]
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
func testNoResultFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = [
{ $0.firstName == "Alex" },
{ $0.hasDriverLicense == false },
{ $0.isAmerican == false },
]
// Then
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, [])
}
func testNoFilters() {
// Given
let peopleViewModel = PeopleViewModel(people: PeopleViewModelTests.people)
// When
peopleViewModel.filters = []
// Then
let expectedResult = PeopleViewModelTests.people
XCTAssertEqual(peopleViewModel.peopleMatchingFilters, expectedResult)
}
}