Testing Models in Swift
What we’ll be learning
Testing functions in our codebase allows us to continuously keep running our tests to ensure our functions all work the same as they did before as more functionality is added.
Testing is a methodology in software development to ensure our code works correctly now and in the future. Codebases are always expanding, and more functionality is added over time. When that happens, there can be situations in which the codebase becomes prone to failing due to missed edge cases. Testing allows us to catch these problems beforehand and cover for them in the future. It enforces the idea of writing good, robust, modular logic.
Unit testing
As its name suggests, unit testing assesses a unit of code — in our case, a function. A function that tests something in our codebase is called a unit test. This unit test is responsible for testing for edge cases, such as:
- Calling a function by passing in an empty array as an argument.
- Making sure, overall, that the code works as expected.
In the long run, test cases help us limit the number of bugs in an application.
For example, the following code creates and test a sum(x:y:)
function:
func sum(x: Int, y: Int) -> Int {return x+y}func testSum() {print(“2 + 4 = 6, \(sum(x: 2, y: 4) == 6)”)print(“3 + 0 = 3, \(sum(x: 3, y: 0) == 3)”)print(“-3 + 0 = -3, \(sum(x: -3, y: 0) == -3)”)}testSum()/* Prints:2 + 4 = 6, true3 + 0 = 3, true-3 + 0 = -3, true*/
In this example, testSum()
acts as a unit test for the sum()
function. It checks some basic cases to ensure that the sum(x:y:)
function works as expected. While the example above just uses print statements, Xcode includes a more robust testing framework that makes it easy to run tests and understand when a test fails.
Creating an Xcode project with unit tests enabled
Let’s go over how we can set up unit tests inside an Xcode project:
- Open Xcode and click the option to “Create a new Xcode project”.
- In the next screen, choose the iOS tab and click on “App”:
- In the following screen, give your project a name and, most importantly, check the box for “Include Tests” and click on “Next”:
- Save and create the project.
Now if you take a look, you’ll notice that your project has automatically added unit tests and UI tests. The words “Tests” and “UITests” should be suffixed to your project name in the containing folders. Because the name of this project is “Testing”, it will automatically create the TestingTests
and TestingUITests
directories.
For the scope of this article, we will be focused on the unit tests which are contained in the folder TestingTests
. If you open it up, you can get a glimpse of how the unit tests are structured. Now that we’re set up with unit tests in Xcode, let’s see how we can use them to test out logic.
Using XCTest
Overview of XCTest starter code
Let’s take a quick look at the starter code that Xcode automatically generates for each test file:
class TestingTests: XCTestCase {override func setUpWithError() throws {// Put setup code here. This method is called before the invocation of each test method in the class.}override func tearDownWithError() throws {// Put teardown code here. This method is called after the invocation of each test method in the class.}func testExample() throws {// This is an example of a functional test case.// Use XCTAssert and related functions to verify your tests produce the correct results.// Any test you write for XCTest can be annotated as throws and async.// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.}func testPerformanceExample() throws {// This is an example of a performance test case.self.measure {// Put the code you want to measure the time of here.}}}
setUpWithError()
- This is a setup method. It lets us initialize variables as needed before each test method is called.tearDownWithError()
- This is a teardown method. It lets us clean up variables, set them tonil
, etc., after each test method is called.testExample()
- This is a basic test method. Making sure our test methods are prefixed withtest
enables it for testing in Xcode.testPerformanceExample()
- This method shows how a performance test measuring time can be written.
Xcode provides us with the XCTest framework that enables running tests seamlessly within our Xcode. XCTest helps us test our app’s codebase by asserting whether a statement is true or not. Looking at the testSum()
example from above, this is how it would look using XCTest:
func testSum() throws {XCTAssert(sum(2, 4) == 6)XCTAssert(sum(3, 0) == 3)XCTAssert(sum(-3, 0) == -3)}
In this case, we use the XCTAssert()
function to test whether our sum()
function passes this test or not.
Testing app logic with our tests
Let’s create a simple UI that takes in 2 numbers as inputs, calculates their sum and prints it out. We’ll then test specific functionality from this view.
Go into ContentView.swift
and replace the starter code with the following code:
import SwiftUIfunc sum(_ x: Int, _ y: Int) -> Int {x+y}struct ContentView: View {@State private var number1: String = ""@State private var number2: String = ""var body: some View {VStack {Form {TextField("Number 1", text: $number1)TextField("Number 2", text: $number2)Text("Sum: \(sum((Int(number1) ?? 0), (Int(number2) ?? 0)))")Spacer()}}}}struct ContentView_Previews: PreviewProvider {static var previews: some View {ContentView()}}
Run the app, this is what you should see:
This code includes the sum()
function we created before. Next, replace testExample()
in TestingTests.swift
with testSum()
:
func testSum() throws {XCTAssert(sum(2, 4) == 6)XCTAssert(sum(3, 0) == 3)XCTAssert(sum(-3, 0) == -3)}
Now let’s run the test. Click on the diamond on the left of the function declaration of testSum()
. That should run the tests. If your `sum(x:y:) function is working correctly, your test should come back as passed with a green checkmark to signify that all the tests passed.
Click on the upper left pane of the “Test Navigator” to see more details about your test. It should be green, indicating that it has passed successfully:
Making tests fail
Next, let’s attempt to make our test fail and pass again. Our goal should be to write unit tests that fail when the behavior of our code changes so that we can catch this and make the necessary adjustments. Refer to this post for more details.
Change the testSum()
method to look like this:
func testSum() throws {XCTAssert(sum(2, 4) == 6)XCTAssert(sum(3, 1) == 3)XCTAssert(sum(-3, 0) == -3)}
Hit the green diamond and see what happens. The second test in this method should be highlighted in red indicating it failed with this error message being displayed in the bottom console:
Test Case ‘-[TestingTests.TestingTests testSum]’ failed (0.118 seconds).
Now let’s go back to what we had before in testSum()
and run the test again:
func testSum() throws {XCTAssert(sum(2, 4) == 6)XCTAssert(sum(3, 0) == 3)XCTAssert(sum(-3, 0) == -3)}
The test should pass now. By making our test fail, then pass again, we can now be sure that the test is working as intended.
Testing with JSON data
Many times, we’re going to have some test data we’d like to run against our code. In this case, we can choose to import a JSON file filled with some mocked data into our tests.
Let’s create a JSON file first:
{"movie_name": "Titanic","release_month": "12","release_day": "19","release_year": "1997"}
- Create an empty file named
data.json
, and fill it with the data above. - Insert the
data.json
file under theTestingTests
folder. - Create this struct for the data model in the
ContentView.swift
file right under thesum(x:y:)
function and right above theContentView
structure:
struct Movie: Decodable {var name: Stringvar releaseDate: Dateenum CodingKeys: String, CodingKey {case movie = "movie_name"case releaseMonth = "release_month"case releaseDay = "release_day"case releaseYear = "release_year"}init(from decoder: Decoder) throws {let values = try decoder.container(keyedBy: CodingKeys.self)name = try values.decode(String.self, forKey: .movie)let releaseMonth = try values.decode(String.self, forKey: .releaseMonth)let releaseDay = try values.decode(String.self, forKey: .releaseDay)let releaseYear = try values.decode(String.self, forKey: .releaseYear)let dateString = "\(releaseMonth)/\(releaseDay)/\(releaseYear)"let dateFormatter = DateFormatter()dateFormatter.dateFormat = "MM/dd/yy"releaseDate = dateFormatter.date(from: dateString) ?? Date()}}
- Next, copy this new test method below in the
TestingTests.swift
file. This test method will do the following:
- Load and read the
data.json
file. - Convert the JSON into the decoded
Movie
model. - Run some tests to make sure our data is accurate.
func testMovie() throws {// 1let bundle = Bundle(for: type(of: self))guard let url = bundle.url(forResource: "data", withExtension: "json") else {XCTFail("Missing file: data.json")return}let json = try Data(contentsOf: url)let decoder = JSONDecoder()// 2let movie = try decoder.decode(Movie.self, from: json)XCTAssertNotNil(movie)XCTAssertEqual(movie.name, "Titanic")let dateFormatter = DateFormatter()dateFormatter.dateFormat = "MM/dd/yy"let date = dateFormatter.date(from: "12/19/1997")XCTAssertEqual(movie.releaseDate, date)}
Let’s go over the function logic:
- Here we load the JSON from the TestingTests bundle. We then decode the JSON string into our
Decodable
type,Movie
. - Once we have
movie
, we validate that it is correct by running a few assertions:
XCTAssertNotNil(movie)
- This verifies that theMovie
type is notnil
.XCTAssertEqual(movie.name, "Titanic")
- This verifies that thename
property is “Titanic”.XCTAssertEqual(movie.releaseDate, date)
- This verifies that thereleaseDate
property is equal to the constructeddate
property (“12/19/1997”).
Note: Some other assertions we can also use:
XCTAssertTrue(movie.name == “Titanic”)
- This verifies that thename
property is “Titanic”.XCTAssertGreaterThan(Date(), movie.releaseDate)
- This verifies that the current date happened after (or is greater than) than the movie release date. In this case Titanic was released in the past before the current date so this is a true statement.
Run the test and it should pass with green being displayed!
Conclusion
Testing is a key component of software development. It gives us the ability to be cognizant of our code by ensuring that it behaves as expected. Keeping a good habit of writing unit tests allows us to write modular pieces of code and ensure that they pass edge cases. As the codebase expands, unit tests limit the number of bugs that come up in the future.
XCTest allows us to assert whether methods are behaving as they’re expected to. This is a very powerful framework that seamlessly integrates with Xcode. There are many different types of assertion functions we can use to validate our function logic. Testing often with the use of XCTest makes sure our codebase is in a healthy state.
Author
'The Codecademy Team, composed of experienced educators and tech experts, is dedicated to making tech skills accessible to all. We empower learners worldwide with expert-reviewed content that develops and enhances the technical skills needed to advance and succeed in their careers.'
Meet the full teamRelated articles
- Article
Introduction to Testing with Mocha and Chai
This article provides a high-level overview of unit testing, why tests are important, and what the Mocha and Chai frameworks provide. - Article
Testing Types
In this article, you will be introduced to the different types of testing that may be used throughout the various stages of a project from local development to shipping to real users.