• Skip to main content
  • Skip to footer

Khawer Khaliq

  • Home

Unit Testing and UI Testing in Swift

Share
Tweet
Share
Pin

This article covers unit testing and UI testing, which together play an important role not only in proving that code is correct at the unit level but also in demonstrating that the application meets user requirements. While unit testing seeks to create a rapid and regular feedback loop for developers to gain confidence in correctness of the code, UI testing validates the application as a whole from an end user’s perspective to ensure that the final product performs as expected by users.

Related article: Test-Driven Development (TDD) in Swift

Contents

What is unit testing
What is UI testing
Test first or last
Naming and documenting tests
What, and what not, to test
Case study
1. Unit testing the model
2. Unit testing the configuration
3. Unit testing the view model
4. Creating the view
5. UI testing the application
Conclusion

What is unit testing

Unit testing is the process of verifying that individual units of code perform as expected. It is the most basic level of testing done by the developers themselves as they write the code. A helpful acronym for desirable properties of unit tests is FIRST. This requires that unit tests should be:

  • (F)ast: Unit tests should execute very quickly so they can be run every time a change is made to the code. If there are hundreds or even thousands of unit tests, they should execute in no more than a few seconds so the tests can be run as often as required.
  • (I)solated: Each unit test should run independently. It should have its own environment, including instances of any required fixtures, not affected by or dependent on any other test.
  • (R)epeatable: Unit tests should be capable of being repeated and the results should not change from one execution to another. This requires that unit tests should not be affected by any environmental factors or dependencies that cannot be controlled.
  • (S)elf-validating: Each unit test should be able to validate on its own, and in an automated manner, whether it passed or not. There should be no need for manual or external validation.
  • (T)horough: Unit tests should validate all possible paths through the code, typically starting with the happy path but also including edge cases, and tests for unexpected or invalid inputs to ensure that the code produces the expected results, including throwing appropriate errors when expected to do so.

What is UI testing

UI testing validates the functionality of the application as presented to users. This enables direct testing of user journeys through the application by simulating user interaction with UI components such as buttons, text fields, option pickers, etc., and validating the state of relevant UI components during and after these interactions. UI tests can, and should, be written directly from user requirements expressed as use cases, user stories, or whatever other artefact is used to capture user requirements.

UI tests can be seen as the ultimate test of functionality and integration. This is because while we can perform unit tests to validate that a unit of code works as expected, or integration tests to validate that certain parts of the application work together, neither unit tests nor integration tests test the application as a whole. If, therefore, we get failing UI tests for a certain user journey when all unit tests and integration tests related to that functionality have passed, it could indicate that the unit or integration tests are not covering the full scope of the required functionality (missing tests) or not testing the correct functionality (incorrect tests).

Unlike unit tests, which typically have a single assertion or a small number of related assertions, UI tests can verify in one test either the presence of all the UI elements expected to be found in one screen (or in one part of the screen for more complex screen layouts) or the steps a user would take to run through a particular journey, by tapping buttons and interacting with other UI elements in the expected way, and verifying results as they appear on the screen. Also unlike unit tests, UI tend to be quite slow since performing a UI test requires the application to be initialized and launched, and the test needs to wait for the operation being performed to complete and the UI to update before performing the required validations.

Test first or last

There is a debate sometimes on whether tests should be written before or after the code to be tested is written. The three common approaches are:

  1. Test last: In this approach, tests are written after the relevant code has been written.
  2. Test first: This approach requires writing tests before the code to be tested is written.
  3. Test-Driven Development (TDD): This is a specific methodology of using tests to drive development of code. TDD leads to writing test and production code in tandem, using small incremental cycles consisting of three steps: Red – write just enough test code until you get a failing test, where failure of the test code to compile is considered a failing test; Green – write just enough production code to make the failing test pass; and, Refactor – improve the code by changing its internal structure without altering its external behaviour, and without breaking any tests.

There are pros and cons of each approach, and the purpose of this article is not to perform a comparative analysis of testing approaches. The bottom line is that whether tests are written before the code is written, as the code is being written, or after the code has been written, it is important that code should be tested thoroughly and regularly to detect issues as early as practically possible rather than being allowed to snowball. It is also important that code is testable by design, and significant refactoring or redesign of production code is not required to be able to test it thoroughly.

Naming and documenting tests

There is no right or wrong way to name tests and document test code. It is important, however, for a developer or development team to adopt appropriate naming and documentation conventions for their test code, and to consistently follow these conventions to make test code clear and readable.

It is useful, and customary, to use a consistent term for the system being tested. A common convention, which will be adopted in this article, is to use the term sut for the ‘system under test’. This will typically be an instance of a struct or a class for unit tests, and an instance of the application for UI tests.

Test code should be distributed among classes in a logical manner, and each test class should be named in a way that clarifies its purpose and the role it plays in the overall scheme of things. The convention that we will follow in this article for unit tests is to use a separate test class for each unit of code to be tested, and for the name of each test class to consist of the name of the unit being tested, followed by the word Tests. For UI tests, the name of each test class will consist of the name of the screen, functionality, or user journey being tested, followed by the term UITests.

Names of test methods provide an opportunity to convey important information about their purpose. In this article, we will make unit test method names fairly descriptive, consisting of at least two and at most three parts, separated by underscores. The first (mandatory) part will be composed of the word test followed by the name of the property or method being tested, and any inputs provided to the method. The second (optional) part will be used for any preconditions of the test. The third (mandatory) part will specify the expected outcome. UI test names in this article will simply consist of the word test followed by the name(s) of the UI component(s) or user interaction(s) being tested.

The above approach can sometimes lead to rather long unit test method names but has the benefit of a reader being able to surmise what each method does without having to read through its implementation. One way to avoid long test method names is to remove the expected outcome from the name of the test method and include it in the assertion(s) made by the test using the optional message parameter, which is provided by all XCTest assertions. These approaches are not mutually exclusive, however. Even if the name of a test method includes its expected outcome, the message can be used to provide further detail or context for an assertion, or any other aspect of the test, that cannot be included in the name of the method.

A typical unit test involves initializing any values and/or fixtures needed by the test and, if required, performing the steps needed for the sut to fulfill any preconditions of the test. Then the test is performed and results are verified. Not all tests include all three steps, however, and some simple tests may just consist of a single assertion. A consistent methodology to delineate these steps can make test code more clear and readable. In this article, the three aforementioned unit test steps, where present, will be indicated using the following terms: Given – for initializing any values or fixtures needed for the test, and performing any actions required to prepare the sut for the test; When – for the step or steps where the sut is exercised as required by the test; and, Then – for verifying the results.

The UI tests we show will also use Given, When, and Then blocks to indicate the preparation, action, and verification steps of a test. By their nature, however, UI tests often tend to test multi-step workflows and validate not only how the UI should look at the end of the workflow but also how it is expected to change during execution of the workflow. As a result, a single UI test may have multiple Given, When or Then blocks to show the steps involved in preparing and testing the workflow, and the results expected at various stages.

What, and what not, to test

Units of code typically expose certain APIs to be used by other parts of the application. Implementation details should be encapsulated so that they can be changed without affecting anything else. A unit of code should be tested only using its exposed API. Any attempt to directly test implementation details will create coupling between tests and these implementation details, causing tests to fail when the implementation is changed as a result of refactoring even though the external behaviour of the unit of code remains unchanged. As a rule of thumb, therefore, unit tests should only have to change when the API exposed by the unit is changed, and not for changes in implementation details.

By the same token, not every unit of code in the application should be tested directly. In Swift, especially when using value types, it is common to use one or more types to compose another type where the types used in the composition exist solely to support the functionality of the composed type, and their existence and use is completely encapsulated. Such types should be considered implementation details, which can be refactored, even inlined, without affecting the external behaviour of the composed type. We may also extend types in the standard library to support some functionality required by the application. In cases where such an extension is created solely to support a particular unit of code, and its existence and use is encapsulated, the extension should be considered an implementation detail of the unit of code it supports.

Such implementation details should not be tested directly. Otherwise, we will get failing tests each time we refactor a unit of code in a way that changes either the implementation of one or more of the types used in its composition or a bespoke extension that it uses. Instead, we should let these implementation details get tested when we test the unit of code that uses them. We will see examples of such implementation details, and how they can be encapsulated, and indirectly tested, when we get into the case study in the next section. Test coverage tools can be used, if required, to confirm that all relevant code is being tested either directly or indirectly.

Case study

In the remainder of this article, we will build a small application from scratch, with unit tests and UI tests. The example application is a simple calculator, which allows the user to enter integers, and perform the four basic arithmetic operations, in the order in which the operations are entered. Our brief is to build the calculator in such a way that it is easy to configure the button layout. This is a subset of the example application used in an earlier article.

The order of presentation of code will be to show each unit of code to be tested along with any types, and extensions on types, that represent its implementation details, followed by the unit tests for that unit. UI tests will be presented after all the code and unit tests have been shown. It must be noted that this is only the order of presentation, and it says nothing about when the various tests were actually written. This is to avoid creating a bias for readers who may subscribe to a particular approach of writing tests.

1. Unit testing the model

We start by looking at the kinds of input that the calculator will handle. This includes:

  • Numeric input in the form of single digits from 0 to 9
  • Arithmetic operations
  • Instructions to evaluate the current operation, and to clear the contents of the calculator

Since the numeric input has only ten possible values, we will create a domain-specific type called Digit, which is implemented by means of an enum, with all possible values as its cases, as shown below.

enum Digit: Int, CaseIterable, CustomStringConvertible {
    case zero, one, two, three, four, five, six, seven, eight, nine
    
    var description: String {
        "\(rawValue)"
    }
}

The cases of Digit get implicitly assigned integer raw values of 0 to 9 respectively. A failable initializer also gets automatically synthesized, which accepts an integer raw value and returns the corresponding Digit value wrapped in an optional if the raw value is valid, and nil otherwise. Digit also conforms to CustomStringConvertible, with the string representation of each case being its integer raw value as a string.

Now let’s look at the tests for the Digit type. In this article, we will present each test class in steps. First, we will show the class with the definition of the sut and any setup and teardown code. Then we will show the test methods in the class one at a time, so the purpose of each method can be discussed.

Testing the Digit type is quite simple. We only need to test this type via its allCases property, which is static and does not require an instance to be created . We also don’t need any setup or teardown code. We just make sut a typealias for the Digit type, as shown in the test class below.

class DigitTests: XCTestCase {
    typealias sut = Digit
}

Now we look at the tests in this class. The first test verifies the presence and ordering of all cases of the enum.

func testAllCases_hasValuesZeroToNineInAscendingOrder() {
    XCTAssertEqual(sut.allCases, [.zero, .one, .two, .three, .four, .five, .six, .seven, .eight, .nine])
}

Next we test the raw value of each case.

func testAllCasesRawValue_hasIntegers0To9InAscendingOrder() {
    XCTAssertEqual(sut.allCases.map({ $0.rawValue }), Array(0...9))
}

Finally, we test the custom string representation of each case.

func testAllCasesCustomStringRepresentation_hasStringsForIntegers0To9InAscendingOrder() {
    XCTAssertEqual(sut.allCases.map(String.init), (0...9).map(String.init))
}

Next, we look at how we will represent arithmetic operations. Since we need to support the four arithmetic operations – addition, subtraction, multiplication, and division – we use an enum here as well.

enum ArithmeticOperation: CaseIterable {
    case addition, subtraction, multiplication, division
}

As with the Digit type, we will test ArithmeticOperation only via its allCases property, so we make sut a typealias for the ArithmeticOperation type, as shown in the test class below.

class ArithmeticOperationTests: XCTestCase {
    typealias sut = ArithmeticOperation
}

Here, the only test we need is for the presence of cases for the four arithmetic operations.

func testAllCases_hasValuesForAdditionSubtractionMultiplicationAndDivision() {
    XCTAssertEqual(sut.allCases, [.addition, .subtraction, .multiplication, .division])
}

Having dealt with the inputs, we move on to the calculator. Shown below is a Calculator type which accepts input in the form of Digit and ArithmeticOperation values, and also accepts instructions to evaluate the present operation, and to clear the contents of the calculator. Here is the code.

struct Calculator: Equatable {
    private var newNumber: Decimal?
    private var expression: ArithmeticExpression?
    private var result: Decimal?

    var number: Decimal? {
        newNumber ?? expression?.number ?? result
    }
    
    mutating func setDigit(_ digit: Digit) {
        guard !(newNumber == nil && digit == .zero) else { return }
        let numberString = newNumber.map() { "\($0)" } ?? ""
        newNumber = Decimal(string: numberString.appending("\(digit)"))
    }

    mutating func setOperation(_ operation: ArithmeticOperation) {
        guard var numberToUse = newNumber ?? result else { return }
        if let existingExpression = expression {
            numberToUse = existingExpression.evaluate(with: numberToUse)
        }
        expression = ArithmeticExpression(number: numberToUse, operation: operation)
        newNumber = nil
    }
    
    mutating func evaluate() {
        guard let numberToUse = newNumber, let expressionToEvaluate = expression else { return }
        result = expressionToEvaluate.evaluate(with: numberToUse)
        newNumber = nil
        expression = nil
    }
    
    mutating func allClear() {
        newNumber = nil
        expression = nil
        result = nil
    }
}

The implementation of Calculator uses an ArithmeticExpression type, which exists solely to support Calculator and its use is completely encapsulated within it. ArithmeticExpression is thus considered an implementation detail of Calculator. We can refactor it in any way, even inline it in Calculator, without affecting any other part of the application as long as the API exposed by Calculator remains unchanged. Accordingly, we include the ArithmeticExpression type definition in the same source file as Calculator and use the private access label to hide it from other parts of the application, as shown below.

private struct ArithmeticExpression: Equatable {
    var number: Decimal
    var operation: ArithmeticOperation
    
    func evaluate(with secondNumber: Decimal) -> Decimal {
        switch operation {
        case .addition:
            return number + secondNumber
        case .subtraction:
            return number - secondNumber
        case .multiplication:
            return number * secondNumber
        case .division:
            return number / secondNumber
        }
    }
}

What this means, as noted earlier in this article, is that we only need to directly test the Calculator type. The ArithmeticExpression type will get tested, along with the other implementation details, as we test the various aspects of the functionality of the Calculator type using its exposed API.

Let us now look at the unit tests for the Calculator type. Unlike the DigitTests and ArithmeticOperationTests classes that we saw earlier, the CalculatorTests class follows a more conventional pattern, where sut is defined as a variable which can hold an instance of Calculator. Note that sut is an implicitly unwrapped optional, which is initialized in the setUp() method, and set to nil in the tearDown() method. This ensures that each test gets its own instance of Calculator, making the tests independent of each other. Here is the test class.

class CalculatorTests: XCTestCase {
    private var sut: Calculator!
    
    override func setUp() {
        sut = Calculator()
    }
    
    override func tearDown() {
        sut = nil
    }
}

The first test in CalculatorTests verifies that the initial value of the number property is nil.

func testNumber_isNil() {
    XCTAssertNil(sut.number)
}

We have a few tests pertaining to setting digits in the calculator. First, we test that when we set a single non-zero digit, the value of the number property is a number consisting of that digit.

func testSetDigitWithNonZeroDigit_createsNumberWithNewDigit() {
    // Given
    let nonZeroDigit: Digit = .five
    // When
    sut.setDigit(nonZeroDigit)
    // Then
    XCTAssertEqual(sut.number, Decimal(nonZeroDigit))
}

Note that we have used an initializer of Decimal that takes a Digit parameter and uses its integer raw value to initialize a Decimal value. This is an example of a helper function, which can make test code simpler and easier to read. We define this convenience initializer for Decimal in the following extension on Decimal included in the test target.

extension Decimal {
    init(_ digit: Digit) {
        init(digit.rawValue)
    }
}

The next test is for setting a non-zero digit when a non-zero digit has previously been set. This appends the new digit to the existing value of the number property. This is the first test where we have used the optional middle part of a test method name to specify the precondition for the test.

func testSetDigitWithNonZeroDigit_nonZeroDigitSet_appendsNewDigitToExistingNumber() {
    // Given
    let firstNonZeroDigit: Digit = .seven
    let secondNonZeroDigit: Digit = .five
    sut.setDigit(firstNonZeroDigit)
    // When
    sut.setDigit(secondNonZeroDigit)
    // Then
    XCTAssertEqual(sut.number, Decimal(string: "\(firstNonZeroDigit)\(secondNonZeroDigit)"))
}

When building a number, the calculator should ignore any leading zero. We test this by setting a zero and checking that the value of the number property remains nil.

func testSetDigitWithZero_hasNilNumber() {
    // When
    sut.setDigit(.zero)
    // Then
    XCTAssertNil(sut.number)
}

While any leading zero should be ignored, non-leading zeroes should be appended to the existing number. We test this next by setting a zero when a non-zero digit has previously been set.

func testSetDigitWithZero_nonZeroDigitSet_appendsZeroToExistingNumber() {
    // Given
    let nonZeroDigit: Digit = .six
    sut.setDigit(nonZeroDigit)
    // When
    sut.setDigit(.zero)
    // Then
    XCTAssertEqual(sut.number, Decimal(string: "\(nonZeroDigit)0"))
}

The next group of tests covers setting and evaluating arithmetic operations.

The first test in this group is for setting an arithmetic operation in a Calculator instance in which a non-zero digit has been set. Note that we assert only that the value of the number property of the Calculator instance remains unchanged, which is all we can observe using the exposed API. This is because, for reasons noted earlier in this article, we will conduct all our unit testing using only the exposed API of the unit being tested, to avoid coupling our tests with implementation details.

func testSetOperation_nonZeroDigitSet_hasUnchangedNumber() {
    // Given
    let nonZeroDigit: Digit = .nine
    sut.setDigit(nonZeroDigit)
    // When
    sut.setOperation(.random)
    // Then
    XCTAssertEqual(sut.number, Decimal(nonZeroDigit))
}

Since the outcome of the above test is not affected by our choice of arithmetic operation, we have set an operation chosen at random from the cases of ArithmeticOperation. This is enabled by the following extension on ArithmeticOperation, which we define in the test target.

extension ArithmeticOperation {
    static var random: ArithmeticOperation {
        allCases[Int.random(in: 0..<allCases.count)]
    }
}

We know that it requires two numbers to execute an arithmetic operation, so the next test sets a non-zero digit in a Calculator instance in which we have previously set a non-zero digit and an arithmetic operation. Again, we assert only that the value of the number property is a number consisting of the new digit.

func testSetNonZeroDigit_nonZeroDigitAndOperationSet_hasNumberWithNewDigit() {
    // Given
    sut = sut.settingDigit(.randomNonZero).settingOperation(.random)
    let newNonZeroDigit: Digit = .three
    // When
    sut.setDigit(newNonZeroDigit)
    // Then
    XCTAssertEqual(sut.number, Decimal(newNonZeroDigit))
}

The outcome of the above test is not affected by the choice of the non-zero digit and the arithmetic operation used to prepare the sut for the test. This is why we have used a randomly chosen arithmetic operation, and a randomly chosen non-zero digit. We had shown earlier the implementation of the method used to get a random arithmetic operation. Shown below is the method used to get a random non-zero digit, which is defined in an extension on Digit included in the test target.

extension Digit {
    static var randomNonZero: Digit {
        let nonZeroCases = allCases.filter() { $0 != .zero }
        return nonZeroCases[Int.random(in: 0..<nonZeroCases.count)]
    }
}

Note the methods settingDigit(_:) and settingOperation(_:) that we have used above to prepare the sut for the test to be performed. We define these helper methods, and two others that we will use later, by means of the following extension on Calculator included in the test target.

extension Calculator {
    func settingDigit(_ digit: Digit) -> Calculator {
        var calculator = self
        calculator.setDigit(digit)
        return calculator
    }
    
    func settingOperation(_ operation: ArithmeticOperation) -> Calculator {
        var calculator = self
        calculator.setOperation(operation)
        return calculator
    }
    
    func evaluating() -> Calculator {
        var calculator = self
        calculator.evaluate()
        return calculator
    }
    
    func clearing() -> Calculator {
        var calculator = self
        calculator.allClear()
        return calculator
    }
}

Finally, we get to the tests that verify the ability of Calculator to evaluate arithmetic operations. Since the same steps will be used for testing the four arithmetic operations, we define a verification method. Verification methods provide a way to encapsulate test steps and assertions to be used in more than one test, to reduce duplication in test code. We define the following verification method in an extension on CalculatorTests, as shown below.

extension CalculatorTests {
    private func verifyEvaluatingOperation(_ operation: ArithmeticOperation, file: StaticString = #file,
                                           line: UInt = #line, action: (Decimal, Decimal) -> Decimal) {
        // Given
        let firstNonZeroDigit: Digit = .eight
        let secondNonZeroDigit: Digit = .four
        sut = sut.settingDigit(firstNonZeroDigit).settingOperation(operation).settingDigit(secondNonZeroDigit)
        // When
        sut.evaluate()
        // Then
        XCTAssertEqual(sut.number, action(Decimal(firstNonZeroDigit), Decimal(secondNonZeroDigit)), file: file, line: line)
    }
}

Note the file and line parameters of the verification method, which are used in the assertion. All XCTest assertions can include these parameters to indicate where a test failure occurred. With the #file and #line default values, the test method that calls the verification method does not need to, and normally should not, specify values for these parameters. By virtue of the default values, any test failure will be reported in the test method at the point where the verification method is called.

Next, we show the methods in CalculatorTests which verify evaluation of the four arithmetic operations, using the above verification method.

func testEvaluate_firstNonZeroDigitAndAdditionOperationAndSecondNonZeroDigitSet_hasNumberWithResultOfAddition() {
    verifyEvaluatingOperation(.addition) { $0 + $1 }
}

func testEvaluate_firstNonZeroDigitAndSubtractionOperationAndSecondNonZeroDigitSet_hasNumberWithResultOfSubtraction() {
    verifyEvaluatingOperation(.subtraction) { $0 - $1 }
}

func testEvaluate_firstNonZeroDigitAndMultiplicationOperationAndSecondNonZeroDigitSet_hasNumberWithResultOfMultiplication() {
    verifyEvaluatingOperation(.multiplication) { $0 * $1 }
}

func testEvaluate_firstNonZeroDigitAndDivisionOperationAndSecondNonZeroDigitSet_hasNumberWithResultOfDivision() {
    verifyEvaluatingOperation(.division) { $0 / $1 }
}

Having shown the tests for setting and evaluating single arithmetic operations, we move on to tests for scenarios where a user wants to chain operations, either by using the result of a previous operation to start a new one or by starting the next operation without having to evaluate the previous one. Accordingly, the next test is for a scenario where the result of evaluating a previous operation is available, which allows an operation to be set without setting a new non-zero digit, and evaluated by setting a single non-zero digit after the operation has been set.

func testSetOperationAndNonZeroDigitAndEvaluate_previousOperationEvaluated_usesPreviousResultForNewOperation() {
    // Given
    sut = sut.settingDigit(.five).settingOperation(.multiplication).settingDigit(.six).evaluating()
    let previousResult = Decimal(.five) * Decimal(.six)
    let newNonZeroDigit: Digit = .three
    // When
    sut.setOperation(.division)
    sut.setDigit(newNonZeroDigit)
    sut.evaluate()
    // Then
    XCTAssertEqual(sut.number, previousResult / Decimal(newNonZeroDigit))
}

Next we show the test for chaining a new operation with the previous one without having to evaluate the previous operation.

func testSetOperationAndNonZeroDigitAndEvaluate_nonZeroDigitAndOperationAndNonZeroDigitSet_evaluatesPreviousOperationAndUsesResultForNewOperation() {
    // Given
    let firstNonZeroDigit: Digit = .eight
    let secondNonZeroDigit: Digit = .two
    sut = sut.settingDigit(firstNonZeroDigit).settingOperation(.addition).settingDigit(secondNonZeroDigit)
    let newNonZeroDigit: Digit = .three
    // When
    sut.setOperation(.multiplication)
    sut.setDigit(newNonZeroDigit)
    sut.evaluate()
    // Then
    XCTAssertEqual(sut.number, (Decimal(firstNonZeroDigit) + Decimal(secondNonZeroDigit)) * Decimal(newNonZeroDigit))
}

The last group of tests in CalculatorTests pertains to being able to clear the contents of the calculator. Note that in all these tests, we use random digits and arithmetic operations since the actual digits and arithmetic operations have no bearing on the outcome of the tests.

The first test is for the scenario where only a non-zero digit has been set.

func testAllClear_nonZeroDigitSet_hasNilNumber() {
    // Given
    sut.setDigit(.randomNonZero)
    // When
    sut.allClear()
    // Then
    XCTAssertNil(sut.number)
}

The next scenario is when a non-zero digit and an operation have been set.

func testAllClear_nonZeroDigitAndOperationSet_hasNilNumber() {
    // Given
    sut = sut.settingDigit(.randomNonZero).settingOperation(.random)
    // When
    sut.allClear()
    // Then
    XCTAssertNil(sut.number)
}

Finally, we look at the scenario where an operation has been evaluated.

func testAllClear_operationEvaluated_hasNilNumber() {
    // Given
    sut = sut.settingDigit(.randomNonZero).settingOperation(.random).settingDigit(.randomNonZero).evaluating()
    // When
    sut.allClear()
    // Then
    XCTAssertNil(sut.number)
}

This completes the tests we need for the model. As noted earlier, it is part of our brief to make the calculator button layout easily configurable. We look at this next.

2. Unit testing the configuration

We will use a ButtonConfiguration type to represent button configuration information, which includes labels for the buttons and the number of columns to be used for button layout. To make button configuration easy to change, we can load configuration information from a file or store configuration preferences in user defaults. To keep things simple, we will load button configuration information from a JSON file. This is done by the ButtonConfiguration type shown below, which conforms to the Decodable protocol, and has a static property called decoded, with a ButtonConfiguration value decoded from the ‘ButtonConfiguration.json’ file in the main bundle.

struct ButtonConfiguration: Equatable, Decodable {
    let labels: [String]
    let columnCount: Int
    
    static let decoded = Bundle.main.decode(ButtonConfiguration.self, from: "ButtonConfiguration.json")
}

The actual decoding is done by the decode(_:from:) method, which we define on the Bundle type by means of the following extension. For reasons discussed earlier, as with other implementation details, the extension is marked private and included in the same source file as ButtonConfiguration.

private extension Bundle {
    func decode<T: Decodable>(_ type: T.Type, from file: String) -> T {
        guard let url = url(forResource: file, withExtension: nil) else {
            fatalError("Unable to locate file: \(file)")
        }
        guard let data = try? Data(contentsOf: url) else {
            fatalError("Unable to load file: \(file)")
        }
        guard let decoded = try? JSONDecoder().decode(T.self, from: data) else {
            fatalError("Unable to decode file: \(file)")
        }
        return decoded
    }
}

The final step is to create a ‘ButtonConfiguration.json’ file in the main bundle, which contains the calculator button labels, in the order in which buttons customarily appear in a calculator keypad, and the actual number of columns to be used for the button layout.

{
    "labels": ["7", "8", "9", "÷", "4", "5", "6", "x", "1", "2", "3", "-", "AC", "0", "=", "+"],
    "columnCount": 4
}

Next we define a test class for ButtonConfiguration. Since we only need to test a static property, we don’t need to create an instance of ButtonConfiguration. Instead, as we had done when we tested the Digit and ArithemticOperation types, we make sut a typealias for the ButtonConfiguration type.

class ButtonConfigurationTests: XCTestCase {
    typealias sut = ButtonConfiguration
}

This class contains a single test to verify that the value of the decoded property has the same button configuration information as in the JSON file in the main bundle.

func testDecoded_hasButtonConfigurationFromJSONFileInMainBundle() {
    // Given
    let buttonConfigurationInJSONFileInMainBundle = ButtonConfiguration(
        labels: ["7", "8", "9", "÷", "4", "5", "6", "x", "1", "2", "3", "-", "AC", "0", "=", "+"],
        columnCount: 4)
    // Then
    XCTAssertEqual(sut.decoded, buttonConfigurationInJSONFileInMainBundle)
}

3. Unit testing the view model

Now we move on to the view model. Note that CalculatorViewModel conforms to the ObservableObject protocol, which will allow the view that we will create later to observe changes in the view model. The calculator property has the @Published property wrapper, which means view updates will be performed when the value of this property changes.

final class CalculatorViewModel: ObservableObject {
    private lazy var commands = {
        buttonLabels.map(CommandFactory.createCommandForLabel)
    }()
    
    @Published private(set) var calculator: Calculator
    
    let buttonLabels: [String]
    let buttonColumnCount: Int

    var displayText: String? {
        calculator.number.map(String.init)
    }
    
    init(calculator: Calculator = Calculator(), buttonConfiguration: ButtonConfiguration = .decoded) {
        self.calculator = calculator
        buttonLabels = buttonConfiguration.labels
        buttonColumnCount = buttonConfiguration.columnCount
    }
    
    func performAction(forButtonLabelIndex index: Int) {
        calculator = commands[index].execute(with: calculator)
    }
}

The CalculatorViewModel initializer takes as its parameters a Calculator value, which it uses to initialize the calculator property, and a ButtonConfiguration value, which it uses to initialize the buttonLabels and buttonColumnCount properties. Both these parameters have default values. The default value for the calculator parameter is a new Calculator instance while the default value for the buttonConfiguration parameter is the value of the decoded property of the ButtonConfiguration type, which we have defined and tested in the last section. This allows us to inject custom Calculator and ButtonConfiguration values for testing the view model while enabling the view model to be initialized in production without any arguments.

Note that CalculatorViewModel uses the Command Pattern to control the calculator. This not only makes the view model simple and easy to understand but also facilitates changing the button configuration, and potentially enhancing calculator functionality in the future, with relatively localized changes to the code. We are using a simple and compact implementation of the Command Pattern in Swift, which provides all the benefits of this classic pattern without the boilerplate code associated with the object-oriented implementation. It also enables us to create commands to work directly with value types, including commands that work with a specific value type as well as generic commands that can work with any value type.

For the purposes of our application, we need commands to invoke methods on instances of the Calculator type, which only has mutating methods. Accordingly, we define a CalculatorCommand type, an instance of which can be used to invoke any mutating method on a value of the Calculator type.

private struct CalculatorCommand {
    typealias MutatingAction = (inout Calculator) -> Void

    private let mutatingAction: MutatingAction
    
    init(_ mutatingAction: @escaping MutatingAction) {
        self.mutatingAction = mutatingAction
    }
    
    func execute(with value: Calculator) -> Calculator {
        var valueToMutate = value
        mutatingAction(&valueToMutate)
        return valueToMutate
    }
}

Each instance of CalculatorCommand stores a closure that takes an inout parameter of the Calculator type and mutates it in place. This closure is provided when the command is initialized. The execute(with:) method creates a mutable copy of the Calculator value provided as its argument, invokes the closure on this mutable value to mutate it in place, and returns the mutated value.

CalculatorViewModel uses the following CommandFactory type to create the required instances of CalculatorCommand.

private struct CommandFactory {
    private static let arithmeticOperationForLabel: [String: ArithmeticOperation] = [
        "+": .addition, "-": .subtraction, "x": .multiplication, "÷": .division]
    
    private static let evaluateLabel = "="
    private static let allClearLabel = "AC"

    static func createCommandForLabel(_ label: String) -> CalculatorCommand {
        if let integer = Int(label), let digit = Digit(rawValue: integer)  {
            return CalculatorCommand() { $0.setDigit(digit) }
        } else if let operation = arithmeticOperationForLabel[label] {
            return CalculatorCommand() { $0.setOperation(operation) }
        } else if label == evaluateLabel {
            return CalculatorCommand() { $0.evaluate() }
        } else if label == allClearLabel {
            return CalculatorCommand() { $0.allClear() }
        } else {
            fatalError("Unable to create command for label: \(label)")
        }
    }
}

Note the private access label used for the CalculatorCommand and CommandFactory types. Since these types exist solely to support the functionality of CalculatorViewModel, they are considered implementation details of CalculatorViewModel. We can refactor them in any way, including inlining them in CalculatorViewModel, without affecting any other part of the application as long as the API exposed by CalculatorViewModel remains unchanged. Accordingly, we include the CalculatorCommand and CommandFactory type definitions in the same source file as CalculatorViewModel and make them private to hide them from other parts of the application.

Next, we show tests for CalculatorViewModel. As with other implementation details, we don’t directly test CalculatorCommand or CommandFactory, which will get tested as we test CalculatorViewModel. Here is the test class.

class CalculatorViewModelTests: XCTestCase {
    private var sut: CalculatorViewModel!
    
    override func setUp() {
        sut = CalculatorViewModel()
    }
    
    override func tearDown() {
        sut = nil
    }
}

The first test in the above class is for the default value of the displayText property, which should be nil.

func testDisplayText_isNil() {
    XCTAssertNil(sut.displayText)
}

The next group of tests validates that the CalculatorViewModel initializer correctly assigns injected values to the corresponding stored properties. We start with a test for the calculator property getting initialized with the value injected through the initializer.

func testCalculator_calculatorValueInjected_hasInjectedValue() {
    // Given
    let calculator = Calculator().settingDigit(.randomNonZero)
    // When
    sut = CalculatorViewModel(calculator: calculator)
    // Then
    XCTAssertEqual(sut.calculator, calculator)
}

The next test verifies that the buttonLabels property gets initialized from the corresponding property of the ButtonConfiguration value injected through the initializer.

func testButtonLabels_buttonConfigurationValueInjected_hasValueFromInjectedButtonConfigurationValue() {
    // Given
    let buttonLabels = ["1", "2", "3"]
    // When
    sut = CalculatorViewModel(buttonConfiguration: ButtonConfiguration(labels: buttonLabels))
    // Then
    XCTAssertEqual(sut.buttonLabels, buttonLabels)
}

Then we test the same for the buttonColumnCount property.

func testButtonColumnCount_buttonConfigurationValueInjected_hasValueFromInjectedButtonConfigurationValue() {
    // Given
    let buttonColumnCount = 2
    // When
    sut = CalculatorViewModel(buttonConfiguration: ButtonConfiguration(columnCount: buttonColumnCount))
    // Then
    XCTAssertEqual(sut.buttonColumnCount, buttonColumnCount)
}

Note that the ButtonConfiguration initializers we have used in the last two tests each take a single parameter. This is so we can focus only on the parameter relevant to the test. But these initializers are not available in the ButtonConfiguration implementation used in production. Since these convenience initializers will be used only for testing, we define them in the following ButtonConfiguration extension included in the test target.

extension ButtonConfiguration {
    init(labels: [String]) {
        init(labels: labels, columnCount: 0)
    }
    
    init(columnCount: Int) {
        init(labels: [], columnCount: columnCount)
    }
}

Next we test the default values assigned to the properties of CalculatorViewModel by its initializer. We start with a test for the default value of the calculator property, which should be a new Calculator instance.

func testCalculator_hasNewInstance() {
    XCTAssertEqual(sut.calculator, Calculator())
}

It is pertinent to note that the remaining tests in CalculatorViewModelTests, which we will present in the remainder of this section, would be called integration tests in the classical sense. This is because classical unit testing isolates the unit being tested from all external dependencies using mocks. This way, the unit can be tested in isolation by directly controlling and/or observing all interactions with the dependencies. Integration tests can then be performed to test how the various parts actually interact by replacing the mocks with production instances of the dependencies.

This is a topic for a separate blog post but suffice it to say that use of mocks for testing in Swift is really required when dealing with resources such as persistent storage, networks, etc., which are usually unsuitable for unit testing because either they are unavailable during testing or they may be too slow or prone to producing unexpected results or unanticipated failures. For dependencies that don’t have such constraints, which would normally be modeled in Swift using value types, it is better to dispense with mocks and use the production instances for testing, which does away with the need for separate integration testing.

Having clarified the approach, we move on to the tests, starting with a test that verifies that the default value assigned to the buttonLabels property of CalculatorViewModel is the value of the labels property of the ButtonConfiguration value decoded from the JSON file.

func testButtonLabels_hasLabelsValueFromDecodedButtonConfiguration() {
    XCTAssertEqual(sut.buttonLabels, ButtonConfiguration.decoded.labels)
}

Like the buttonLabels property, the buttonColumnCount property also has its default value assigned using the decoded ButtonConfiguration value, which is the subject of the next test.

func testButtonColumnCount_hasColumnCountValueFromDecodedButtonConfiguration() {
    XCTAssertEqual(sut.buttonColumnCount, ButtonConfiguration.decoded.columnCount)
}

Having tested the initializer, we test the ability of CalculatorViewModel to perform the expected actions on its Calculator instance. While all these tests call the performAction(forButtonLabelIndex:) method, which invokes the command corresponding to the integer index passed as its argument, the main functionality being tested here comes from the CalculatorCommand and CommandFactory types used by CalculatorViewModel.

Our first test injects via the CalculatorViewModel initializer a ButtonConfiguration value with a buttonLabels array comprising string representations of all Digit cases in reverse order. It then calls the performAction(forButtonLabelIndex:) method with the indices of all these button labels, and verifies that the corresponding digits are set in the Calculator instance of the view model.

func testPerformAction_digitButtonLabelsValueInjected_setsDigitsInCalculator() {
    // Given
    let digits = Digit.allCases.reversed()
    sut = CalculatorViewModel(buttonConfiguration: ButtonConfiguration(labels: digits.map(String.init)))
    // When
    (0..<digits.count).forEach(sut.performAction)
    // Then
    XCTAssertEqual(sut.calculator, digits.reduce(into: Calculator(), { $0.setDigit($1) }))
}

Next we test that the view model can similarly set an arithmetic operation in its Calculator instance when we inject the corresponding button label, and perform an action with the index of this button label. Note that this requires also injecting a Calculator instance in which we have set a non-zero digit. This is because setting an operation in a Calculator instance requires a number to be present in its number property. Since tests for the four arithmetic operations will follow the same pattern, we create a verification method in the following extension on CalculatorViewModelTests.

extension CalculatorViewModelTests {
    private func verifyActionForOperation(_ operation: ArithmeticOperation, withButtonLabel buttonLabel: String,
                                          file: StaticString = #file, line: UInt = #line) {
        // Given
        let calculator = Calculator().settingDigit(.randomNonZero)
        sut = CalculatorViewModel(
            calculator: calculator,
            buttonConfiguration: ButtonConfiguration(labels: ["\(buttonLabel)"]))
        // When
        sut.performAction(forButtonLabelIndex: 0)
        // Then
        XCTAssertEqual(sut.calculator, calculator.settingOperation(operation), file: file, line: line)
    }
}

We show below the methods in CalculatorViewModelTests that use the above method to verify that the view model correctly sets each arithmetic operation.

func testPerformAction_additionButtonLabelInjected_setsAdditionOperationInCalculator() {
    verifyActionForOperation(.addition, withButtonLabel: "+")
}

func testPerformAction_subtractionButtonLabelInjected_setsSubtractionOperationInCalculator() {
    verifyActionForOperation(.subtraction, withButtonLabel: "-")
}

func testPerformAction_multiplicationButtonLabelInjected_setsMultiplicationOperationInCalculator() {
    verifyActionForOperation(.multiplication, withButtonLabel: "x")
}

func testPerformAction_divisionButtonLabelInjected_setsDivisionOperationInCalculator() {
    verifyActionForOperation(.division, withButtonLabel: "÷")
}

The next test follows the same pattern to validate the ability of CalculatorViewModel to evaluate its Calculator instance.

func testPerformAction_evaluateButtonLabelInjected_evaluatesCalculator() {
    // Given
    let calculator = Calculator().settingDigit(.six).settingOperation(.addition).settingDigit(.four)
    sut = CalculatorViewModel(
        calculator: calculator,
        buttonConfiguration: ButtonConfiguration(labels: ["="]))
    // When
    sut.performAction(forButtonLabelIndex: 0)
    // Then
    XCTAssertEqual(sut.calculator, calculator.evaluating())
}

Similarly, we test that CalculatorViewModel can clear its Calculator instance.

func testPerformAction_allClearButtonLabelInjected_clearsCalculator() {
    // Given
    let calculator = Calculator().settingDigit(.randomNonZero)
    sut = CalculatorViewModel(
        calculator: calculator,
        buttonConfiguration: ButtonConfiguration(labels: ["AC"]))
    // When
    sut.performAction(forButtonLabelIndex: 0)
    // Then
    XCTAssertEqual(sut.calculator, calculator.clearing())
}

The final view model test validates that the displayText property of CalculatorViewModel shows a string representation of the value of the number property of its Calculator instance. We do this by injecting a single digit label through the view model initializer, performing an action with this label, and verifying that value of the displayText property of the view model is equal to the string representation of the number property of a Calculator instance in which the same digit has been set.

func testDisplayText_actionPerformedForDigitLabel_hasNumberOfCalculatorWithSameDigitSet() {
    // Given
    let digit = Digit.randomNonZero
    sut = CalculatorViewModel(buttonConfiguration: ButtonConfiguration(labels: ["\(digit)"]))
    sut.performAction(forButtonLabelIndex: 0)
    // Then
    XCTAssertEqual(sut.displayText, Calculator().settingDigit(digit).number.map(String.init))
}

4. Creating the view

Our simple application needs a single view, which is shown below.

struct ContentView: View {
    @StateObject var viewModel = CalculatorViewModel()
    
    var buttonColumns: Array<GridItem> {
        Array(repeating: GridItem(.flexible()), count: viewModel.buttonColumnCount)
    }

    var body: some View {
        VStack {
            Text(viewModel.displayText ?? " ")
                .displayText()
            LazyVGrid(columns: buttonColumns) {
                ForEach(0..<viewModel.buttonLabels.count, id: \.self) { index in
                    Button(viewModel.buttonLabels[index]) {
                        viewModel.performAction(forButtonLabelIndex: index)
                    }
                    .buttonLabel()
                }
            }
        }
        .padding()
    }
}

The view uses view modifiers to format the text which represents the calculator display, and the button labels. Using view modifiers in this way helps keep formatting details separate from the view layout code, making code easier to read and facilitating consistent formatting of views. Since these view modifiers are used only by ContentView, they are implementation details of ContentView. Accordingly, and in keeping with our approach of encapsulating implementation details, we mark the view modifier types private and include them in the same source file as ContentView.

Show below is the view modifier used for the display text.

private struct DisplayText: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .padding()
            .frame(maxWidth: .infinity, alignment: .trailing)
            .background(Color(.label))
            .foregroundColor(Color(.systemBackground))
            .lineLimit(1)
            .cornerRadius(5)
    }
}

Here is the view modifier used to format button labels.

private struct ButtonLabel: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.title2)
            .padding()
            .frame(maxWidth: .infinity, alignment: .center)
            .background(Color(.systemGray3))
            .foregroundColor(Color(.label))
            .cornerRadius(5)
    }
}

The following extension on View makes these view modifiers easier and more natural to use at the call site.

private extension View {
    func displayText() -> some View {
        modifier(DisplayText())
    }
    
    func buttonLabel() -> some View {
        modifier(ButtonLabel())
    }
}

Note that the way the application has been designed, there is no business logic in the view to be tested. The view creates an instance of the view model and connects the various UI components to the relevant properties and methods in the view model. Whether these connections have been made correctly will be validated when we perform UI testing in the next section.

5. UI testing the application

We use two test classes in our UI testing, one to test the UI components on the main screen, and the other to test the workflows. This may seem like overkill for this simple application, and it likely is. However, the separation makes sense for larger applications with many screens and more complex screen layouts, so it has been shown here for the sake of demonstrating the methodology.

The first UI test class, which is shown below, tests the main screen of the calculator to verify that the expected UI components are present. Note that the ‘system under test’ in UI test classes is the application, an instance of which is created and launched in the setUp() method, and set to nil in the tearDown() method.

class MainScreenUITests: XCTestCase {
    private var sut: XCUIApplication!

    override func setUp() {
        continueAfterFailure = false
        sut = XCUIApplication()
        sut.launch()
    }
    
    override func tearDown() {
        sut = nil
    }
}

The only test in this class verifies that the main screen has an empty display, one button each for the digits 0 to 9, and buttons to evaluate and clear the calculator.

func testDisplayAndButtonLabels() {
    // Given
    let buttonLabels = (0...9).map(String.init) + ["+", "-", "x", "÷", "=", "AC"]
    // Then
    XCTAssertEqual(sut.staticTexts.count, 1)
    XCTAssertEqual(sut.buttons.count, buttonLabels.count)
    XCTAssertTrue(sut.staticTexts[" "].exists)
    for buttonLabel in buttonLabels {
        XCTAssertTrue(sut.buttons[buttonLabel].exists)
    }
}

The various calculation workflows are tested in a separate UI test class. This includes tests for entering numbers, evaluating and chaining operations, and clearing the contents of the calculator. Here is the test class.

class CalculationUITests: XCTestCase {
    private var sut: XCUIApplication!

    override func setUp() {
        continueAfterFailure = false
        sut = XCUIApplication()
        sut.launch()
    }
    
    override func tearDown() {
        sut = nil
    }
}

The first test in this class validates using the digit buttons to enter a number, which gets built in the display as each digit button is tapped.

func testNumberEntry() {
    // Given
    let buttonLabels = Array(0...9).reversed().map(String.init)
    for (index, buttonLabel) in buttonLabels.enumerated() {
        // When
        sut.buttons[buttonLabel].tap()
        // Then
        let numberInDisplay = buttonLabels.prefix(through: index).joined()
        XCTAssertTrue(sut.staticTexts[numberInDisplay].exists)
    }
}

The next test verifies that, as arithmetic operations are entered and evaluated, the correct number appears in the display at each step.

func testArithmeticOperationSteps() {
    // Given
    let firstDigit = 8
    let secondDigit = 2
    let operationSymbols = ["+", "-", "x", "÷"]
    let expectedResults = [
        firstDigit + secondDigit,
        firstDigit - secondDigit,
        firstDigit * secondDigit,
        firstDigit / secondDigit]
    for (operationSymbol, expectedResult) in zip(operationSymbols, expectedResults) {
        // When (first digit button tapped)
        sut.buttons["\(firstDigit)"].tap()
        // Then (first digit appears in the display)
        XCTAssertTrue(sut.staticTexts["\(firstDigit)"].exists)
        // When (operation button tapped)
        sut.buttons[operationSymbol].tap()
        // Then (first digit remains in the display)
        XCTAssertTrue(sut.staticTexts["\(firstDigit)"].exists)
        // When (second digit button tapped)
        sut.buttons["\(secondDigit)"].tap()
        // Then (second digit appears in the display)
        XCTAssertTrue(sut.staticTexts["\(secondDigit)"].exists)
        // When (evaluate button tapped)
        sut.buttons["="].tap()
        // Then (calculation result appears in the display)
        XCTAssertTrue(sut.staticTexts["\(expectedResult)"].exists)
    }
}

Note that we test all four arithmetic operations in a single test. This is because, unlike unit tests which run fast, UI tests run slowly as the application needs to be launched to run each test. This is why UI tests often combine elements of functionality that we would test in separate unit tests, as we had done when testing evaluation of the arithmetic operations by the Calculator and CalculatorViewModel types.

The next two tests cover chaining operations. First, we test that a user can start a new operation by using the result of a previous operation.

func testStartNewOperationUsingResultOfPreviousOperation() {
    // Given
    let previousFirstDigit = 5
    let previousSecondDigit = 4
    sut.buttons["\(previousFirstDigit)"].tap()
    sut.buttons["+"].tap()
    sut.buttons["\(previousSecondDigit)"].tap()
    sut.buttons["="].tap()
    let previousResult = previousFirstDigit + previousSecondDigit
    XCTAssertTrue(sut.staticTexts["\(previousResult)"].exists)
    let newDigit = 3
    // When
    sut.buttons["-"].tap()
    sut.buttons["\(newDigit)"].tap()
    sut.buttons["="].tap()
    // Then
    XCTAssertTrue(sut.staticTexts["\(previousResult - newDigit)"].exists)
}

Then we test that a user can start a new operation without having to evaluate the previous one, which gets automatically evaluated, and its result is used in the new operation.

func testStartNewOperationWithoutEvaluatingPreviousOperation() {
    // Given
    let previousFirstDigit = 5
    let previousSecondDigit = 4
    sut.buttons["\(previousFirstDigit)"].tap()
    sut.buttons["+"].tap()
    sut.buttons["\(previousSecondDigit)"].tap()
    let newDigit = 3
    // When
    sut.buttons["-"].tap()
    sut.buttons["\(newDigit)"].tap()
    sut.buttons["="].tap()
    // Then
    XCTAssertTrue(sut.staticTexts["\(previousFirstDigit + previousSecondDigit - newDigit)"].exists)
}

Finally, we test the ability to clear the calculator. We simulate tapping the ‘AC’ button at various stages of entering and evaluating a calculation, and verify that the display of the calculator is clear.

func testClearCalculator() {
    // Given
    sut.buttons["5"].tap()
    // When
    sut.buttons["AC"].tap()
    // Then
    XCTAssertTrue(sut.staticTexts[" "].exists)
    // Given
    sut.buttons["5"].tap()
    sut.buttons["+"].tap()
    // When
    sut.buttons["AC"].tap()
    // Then
    XCTAssertTrue(sut.staticTexts[" "].exists)
    // Given
    sut.buttons["5"].tap()
    sut.buttons["+"].tap()
    sut.buttons["4"].tap()
    sut.buttons["="].tap()
    // When
    sut.buttons["AC"].tap()
    // Then
    XCTAssertTrue(sut.staticTexts[" "].exists)
}

Note that, as with testing the arithmetic operations, we have combined the three scenarios into a single UI test whereas we had tested the same scenarios with separate unit tests. We have done this to avoid having too many UI tests bearing in mind the time taken by UI tests to execute.

Conclusion

Xcode provides a number of features to facilitate unit testing and UI testing. Whether we write test before, after, or in tandem with the code to be tested, it is important to have unit tests for various paths through our code, including edge cases and potential error conditions. Unit tests give developers a higher degree of confidence in correctness of the code. Having a comprehensive suite of unit tests that validate the external behaviour of the code without being coupled to its implementation also gives developers the ability to refactor the code taking advantage of the safety net provided by the tests. UI tests enable testing of the application from a user’s perspective, by simulating user interactions with relevant UI elements and validating the state of the UI during and after these interactions.

Thank you for reading! I always appreciate constructive comments and feedback. Please feel free to leave a comment in the comments section of this post, or start a conversation on Twitter.

Subscribe to get notifications of new posts

No spam. Unsubscribe any time.

Reader Interactions

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Footer

Protocol-Oriented Programming (POP) in Swift

Use protocol-oriented programming to think about abstractions in a completely different way, leveraging retroactive modeling to introduce appropriate abstractions at any point in the development cycle, and creating traits that can let types opt into functionality simply by conforming to a protocol.

Pattern Matching With Optionals in Swift

The optional pattern explained in detail, including how to use it with a variety of conditional statements and loops, and how to add extra conditions when required, with a section on creating more complex pattern matching code involving optionals.

Test-Driven Development (TDD) in Swift

Learn how to use Test-Driven Development (TDD) in Swift which not only enables you to write more reliable and maintainable code but also allows refactoring of code in small increments and with greater ease and confidence.

Unwrapping Optionals With Optional Binding in Swift

Learn how to use optional binding to extract the value wrapped by an optional to a constant or variable, as part of a conditional statement or loop, exploring where optional chaining may be used in place of optional binding, and where these techniques can be used together.

When and How to Use the Equatable and Identifiable Protocols in Swift

Detailed coverage of the Equatable and Identifiable protocols, and how they can be used to model not only values but also domain entities with identity using Swift value types, to create code that is more efficient, easier to reason about, easily testable and more concurrency-friendly.

The Power of Optional Chaining in Swift

Learn how to use optional chaining to work safely with optionals, to set and retrieve the value of a property of the wrapped instance, set and retrieve a value from a subscript on the wrapped instance, and call a method on the wrapped instance, all without having to unwrap the optional.

What Are Swift Optionals and How They Are Used

This article explains what Swift optionals are, why Swift has them, how they are implemented, and how Swift optionals can be used to better model real-world domains and write safer and more expressive code.

A Protocol-Oriented Approach to Associated Types and Self Requirements in Swift

Use protocol-oriented programming to avoid having to use associated types in many situations but also to effectively use associated types and Self requirements, where appropriate, to leverage their benefits while avoiding the pitfalls.

Encapsulating Domain Data, Logic and Business Rules With Value Types in Swift

Leverage the power of Swift value types to manage domain complexity by creating rich domain-specific value types to encapsulate domain data, logic and business rules, keeping classes lean and focused on maintaining the identity of entities and managing state changes through their life cycles.

Rethinking Design Patterns in Swift – State Pattern

The State pattern, made simpler and more flexible with the power of Swift, with a detailed worked example to illustrate handling of new requirements, also looking at key design and implementation considerations and the benefits and practical applications of the pattern.

Conditional Logic With and Without Conditional Statements in Swift

Shows how to implement conditional logic using conditional statements as well as data structures, types and flow control mechanisms such as loops, balancing simplicity and clarity with flexibility and future-proofing.

Better Generic Types in Swift With the Numeric Protocol

Covers use of the Numeric protocol as a constraint on type parameters of generic types to ensure that certain type parameters can only be used with numeric types, using protocol composition to add relevant functionality as required.

Understanding, Preventing and Handling Errors in Swift

Examines the likely sources of errors in an application, some ways to prevent errors from occurring and how to implement error handling, using the error handling model built into Swift, covering the powerful tools, associated techniques and how to apply them in practice to build robust and resilient applications.

When and How to Use Value and Reference Types in Swift

Explores the semantic differences between value and reference types, some of the defining characteristics of values and key benefits of using value types in Swift, leading into a discussion on how value and reference types play a complementary role in modeling real-world domains and designing applications.

Swift Protocols Don’t Play Nice With Equatable. Or Can They? (Part Two)

Uses type erasure to implement Equatable conformance at the protocol level, allowing us to program to abstractions using protocol types while safely making equality comparisons and using functionality provided by the Swift Standard Library only available to types that conform to Equatable.

Copyright © Khawer Khaliq, 2017-25. All rights reserved.