Testing Redux-based iOS app.

f:id:damianp:20191226153929p:plain
Every developer is dreaming about well written, bug-less and easy to maintain code - at least everyone should. A very convenient way to ensure that our code is like the one described in the previous sentence is to do unit testing. Wikipedia's define unit testing as follows:

In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use.

In this article, we will try to fit into this definition by testing the Shopping List app from my last article (source code). Let's get started!

Warmup

Let's start with something simple and tests Item model, whether these struct instances are truly unique and values that we passed in the init method matches the instance's values.
To start testing we have to just add a Unit Test class to the shoppingListTests folder and name it ItemTests. Inside the newly created class, we need to add @testable import shoppingList just below the import XCTest. This statement is basically importing our module and allow us to tests internal entities. Before we jump to the code, let's first get familiar with one more thing - SUT. SUT is the acronym for the System Under Test (sometimes also called CUT - Class Under Test) and it refers to the system (or class) that is being tested. Now we are ready and we can create our first test case by creating a function called test_item().

import XCTest
@testable import shoppingList
class ItemTests: XCTestCase {
func test_item_init() {
let name = "fake item"
let date = Date(timeIntervalSince1970: 9999)
let priority = Priority.medium
let sutItem = Item(name: name, date: date, priority: priority)
XCTAssertEqual(sutItem.name, name)
XCTAssertEqual(sutItem.date, date)
XCTAssertEqual(sutItem.priority, priority)
}
}

In the beginning, we are creating the name, date, priority and sut variables. The first three are the data that will feed the Item struct and the last one is our SUT. Next we are calling the XCTAssertEqual that is a part of the XCTest framework to test our Item. First parameter is the value that we are testing and the second one is the expected value. When these two parameters are the same, the test succeed, when they aren't, the test failed. Parameters order doesn't matter. Now, let's try to test the items uniqueness.

func test_item_notEqual() {
let name = "fake item"
let date = Date(timeIntervalSince1970: 9999)
let priority = Priority.medium
let sutItem = Item(name: name, date: date, priority: priority
let notExpectedItem = Item(name: name, date: date, priority: priority)
XCTAssertNotEqual(sutItem, notExpectedItem)
}

We are creating two items - sutItem and notExpectedItem that supposed to be the different items (despite having the same input) because of the id field that is inaccessible in the init method and it is automatically generated. The XCTAssertNotEqual is very similar to the previously used XCTAssertEqual, the only difference is that the parameters must not be the same. When we try to run this test, we should get the compiler error saying:

Global function 'XCTAssertNotEqual(::_:file:line:)' requires that 'Item' conform to 'Equatable'

We don't have any other choice, so let's make the Item conform to the Equatable protocol!

extension Item: Equatable {
public static func == (lhs: Item, rhs: Item) -> Bool {
lhs.date == rhs.date &&
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.priority == rhs.priority
}
}

As you can see, it's very simple. We just have to add one extension at the end of the file and the previously broken test should run smoothly!
These tests look good but before we go to Redux testing, I want to add one more test - whether the items are equal. To be able to test it we have to slightly modify the Item struct init method, to add the id to the init parameters.

import Foundation
struct Item: Identifiable {
let id: UUID
let name: String
let date: Date
let priority: Priority
init(id: UUID = .init(), name: String, date: Date, priority: Priority) {
self.id = id
self.name = name
self.date = date
self.priority = priority
}
}

Newly added parameter id has a default value which means that we don't have to change any, previously written code and at the same time, we made the Item more testable! Let's use this and add the last test.

 func test_item_equal() {
let name = "fake item"
let date = Date(timeIntervalSince1970: 9999)
let priority = Priority.medium
let uuid = UUID()
let sutItem = Item(id: uuid, name: name, date: date, priority: priority)
let expectedItem = Item(id: uuid, name: name, date: date, priority: priority)
XCTAssertEqual(sutItem, expectedItem)
}

In this test, we have one additional variable - uuid - that is passed as a id parameter to the sutItem and expectedItem. Now we can hit the ⌘ + U and watch how tests are succeeding ✅✅✅.

Redux

Redux as architecture is predictable, for a certain input, we should receive a certain output, more precisely, for the same input, we should receive the same output. It's consistent and keeping this in mind, let's start with testing Actions.
First of all we need to create an AppActionTests file. In the Shopping List app we have three actions: addItem, removeItem and sort and to be able to test them, the AppAction enum has to conform to the Equatable protocol. We can achieve it this the same way we did with the Item struct so let's add this extension at the end of the file:

extension AppAction: Equatable {
public static func == (lhs: AppAction, rhs: AppAction) -> Bool {
switch (lhs, rhs) {
case let (.addItem(itemA), .addItem(item: itemB)):
return itemA == itemB
case let (.removeItem(indexA), .removeItem(indexB)):
return indexA == indexB
case let (.sort(typeA), .sort(typeB)):
return typeA == typeB
default:
return false
}
}
}

Now we can create the test cases, let's start with addItem action.

func test_addItem() {
let item = Item(name: "MockItem", date: .init(timeIntervalSinceNow: -2000), priority: .medium)
let sutAction = AppAction.addItem(item: item)
XCTAssertEqual(sutAction, .addItem(item: item))
}

First of all, we have to create an item variable, that will be an action parameter and then we can create an sutAction that is addItem action. In the next step we simply using the previously used XCTAssertEqual passing the sutAction and the addItem action with the item as a parameter.

Testing the removeItem is really similar.

func test_removeItem() {
let index = IndexSet(arrayLiteral: 99)
let sutAction = AppAction.removeItem(at: index)
XCTAssertEqual(sutAction, .removeItem(at: index))
}

In the beginning we are creating an index variable that is the sutAction parameter and once again we are using XCTAssertEqual to perform the test.

The last action test is slightly different.

func test_sort() {
var sortType = SortType.priority
var sutAction = AppAction.sort(by: sortType)
XCTAssertEqual(sutAction, .sort(by: sortType))
sortType = .date
sutAction = .sort(by: sortType)
XCTAssertEqual(sutAction, .sort(by: sortType))
}

This time we are creating mutable variables sortType and sutAction and then as usual we are using XCTAssertEqual to perform test. In the next step, we are changing the variable's values and perform test once again. Now we can hit the ⌘ + U once again and watch how tests are succeeding ✅✅✅.

Let's create test cases for the State. As usual, we have to create a new file - AppStateTests. Our AppState doesn't conform to the Equatable protocol so we have to fix it by adding the following extension in th end of the file:

extension AppState: Equatable {
public static func == (lhs: AppState, rhs: AppState) -> Bool {
lhs.items == rhs.items &&
lhs.sortType == rhs.sortType
}
}

Now we can create our test cases:

import XCTest
@testable import shoppingList
class AppStateTests: XCTestCase {
let items = [Item(name: "fake item", date: .init(timeIntervalSinceNow: -10), priority: .high),
Item(name: "fake item", date: .init(timeIntervalSince1970: 10), priority: .low)]
func test_state_equal() {
let sutState = AppState(items: items, sortType: .priority)
XCTAssertEqual(sutState, AppState(items: items, sortType: .priority))
}
func test_state_notEqual() {
let sutState = AppState(items: items.reversed(), sortType: .date)
XCTAssertNotEqual(sutState, AppState(items: items, sortType: .date))
}
}

At first, we have to create an items array, available in the entire class, because we are using it in both cases. In the test_state_equal function we are creating the sutState with items and priority sort type as a parameters and once again we are using XCTAssertEqual to perform the test. In the test_state_notEqual function, the sutState uses reversed items array and date sorting type as parameters because we want to see if states aren't always equal. Also this time we are using XCTAssertNotEqual to perform the test.

Now, let's test Reducer. Let's create a new name it AppReducerTests. Testing the reducer is basically testing its output (a new state) at the set input parameters (an old state and an action), keeping this in mind, we can add the following lines to the newly created class.

import XCTest
@testable import shoppingList
class AppReducerTests: XCTestCase {
let items = [Item(name: "fake item", date: .init(timeIntervalSinceNow: -10), priority: .low),
Item(name: "fake item", date: .init(timeIntervalSince1970: 10), priority: .high)]
let sortType = SortType.date
lazy var oldState = AppState(items: items, sortType: sortType)
func test_addItem() {
let item = Item(name: "fake item", date: .init(timeIntervalSince1970: 999), priority: .medium)
let expectedState = AppState(items: items + [item], sortType: sortType)
XCTAssertEqual(appReducer(state: oldState, action: .addItem(item: item)), expectedState)
}
func test_removeItem() {
let expectedState = AppState(items: [items[1]], sortType: sortType)
let index = IndexSet(arrayLiteral: 0)
XCTAssertEqual(appReducer(state: oldState, action: .removeItem(at: index)), expectedState)
}
func test_sort() {
var sort = SortType.priority
var expectedState = AppState(items: items.reversed(), sortType: sort)
XCTAssertEqual(appReducer(state: oldState, action: .sort(by: sort)), expectedState)
sort = SortType.date
expectedState = AppState(items: items, sortType: sort)
XCTAssertEqual(appReducer(state: oldState, action: .sort(by: sort)), expectedState)
}
}

First of all, we are creating common, reusable variables - items, sortType and state and then testing each possible combination of appReducer's parameters - in our case the number of combination is three because we have three possible state mutations (actions).
In each case, we are creating an expectedState variable that is - as you may expect - a new, expected state. That means that we have to know what kind of mutation we are expecting and we know how the state should look like. In the next step, we are using well known XCTAssertEqual with appropriate parameters for each test. Now we can hit the ⌘ + U once again and watch how tests are succeeding ✅✅✅.

The last but not least thing that we want to test is Store. In this test, we want to perform multiple actions and in the end, we want to know whether the final state is as we expected. Let's create a new file, named it AppStoreTests and add following code.

import XCTest
@testable import shoppingList
class AppStoreTests: XCTestCase {
func test_store() {
let store = AppStore()
let item1 = Item(name: "fake item1", date: Date(), priority: .low)
let item2 = Item(name: "fake item2", date: Date(timeIntervalSince1970: 10000), priority: .medium)
let item3 = Item(name: "fake item3", date: Date(timeIntervalSinceNow: -10000), priority: .high)
store.dispatch(action: .addItem(item: item1))
store.dispatch(action: .addItem(item: item2))
store.dispatch(action: .addItem(item: item3))
store.dispatch(action: .sort(by: .priority))
store.dispatch(action: .removeItem(at: .init(arrayLiteral: 2)))
store.dispatch(action: .sort(by: .date))
let expectedState = AppState(items: [item3, item2], sortType: .date)
XCTAssertEqual(store.state, expectedState)
}
}

This test case heavily based dispatch function, that is the only way to perform mutation on our state. For the last time, we can hit ⌘ + U and watch how tests are succeeding ✅✅✅.

Conclusion

In my last article, I mentioned that testing the Redux-based app is really simple. The test cases are predictable and easy to write. Of course for a more complex app, there are a lot more test cases and they are much more complicated but still, they are based on the same schema - we have an input and we know how the output should look like so we can generate it and test these two using XCTAssertEqual or any other XCTest based method.

I hope you enjoy the post and feel free to reach me out on twitter. Happy Saturday!

Resources & Useful links

Redux - A predictable state container for JavaScript apps.

https://developer.apple.com/documentation/xctest