Testing Models in Swift

Codecademy Team
Learn how to test your code so that it can withstand changes to the codebase in the future.

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, true
3 + 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:

  1. Open Xcode and click the option to “Create a new Xcode project”.
  2. In the next screen, choose the iOS tab and click on “App”: step 2
  3. In the following screen, give your project a name and, most importantly, check the box for “Include Tests” and click on “Next”: step 3
  4. 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.

project

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.
}
}
}
  1. setUpWithError() - This is a setup method. It lets us initialize variables as needed before each test method is called.
  2. tearDownWithError() - This is a teardown method. It lets us clean up variables, set them to nil, etc., after each test method is called.
  3. testExample() - This is a basic test method. Making sure our test methods are prefixed with test enables it for testing in Xcode.
  4. 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 SwiftUI
func 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: screenshot

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:

image5

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"
}
  1. Create an empty file named data.json, and fill it with the data above.
  2. Insert the data.json file under the TestingTests folder.
  3. Create this struct for the data model in the ContentView.swift file right under the sum(x:y:) function and right above the ContentView structure:
struct Movie: Decodable {
var name: String
var releaseDate: Date
enum 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()
}
}
  1. 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 {
// 1
let 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()
// 2
let 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:

  1. Here we load the JSON from the TestingTests bundle. We then decode the JSON string into our Decodable type, Movie.
  2. Once we have movie, we validate that it is correct by running a few assertions:
  • XCTAssertNotNil(movie) - This verifies that the Movie type is not nil.
  • XCTAssertEqual(movie.name, "Titanic") - This verifies that the name property is “Titanic”.
  • XCTAssertEqual(movie.releaseDate, date) - This verifies that the releaseDate property is equal to the constructed date property (“12/19/1997”).

Note: Some other assertions we can also use:

  • XCTAssertTrue(movie.name == “Titanic”) - This verifies that the name 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.