Generating mock classes for unit testing in Swift

Using mocks for unit testing makes it easy to write test cases without configuring actual dependencies. However, it is hard to manually write and update all the mocking classes whenever there is a change. Here, Sourcery can help generate them automatically with a command.

Productivity

Modern software is written in an object oriented paradigm where it consists of multiple objects dedicated to serve specific purposes. The foundation of testing those objects is unit testing. A unit test case is written by asserting desired side-effects after an operation is done to an object. To effectively test these objects, you have to follow certain steps to ensure proper setup and functionality.

Let’s think about a unit component for a counter app with an increment button. We need an object which manipulates and keeps track of counting. It is the core of the app encapsulating the main business logic. protocol CounterDelegate: AnyObject {

func counter(_ counter: Counter, didUpdateCount count: Int)


} class Counter {

weak var delegate: CounterDelegate?

private(set) var count = 0 {
    didSet { delegate?.counter(self, didUpdateCount: count) }
}

func increment() {
    count = 1
}


The Counter class has an integer property to store state and a function incrementing it. On calling increment function we expect it to update count property and notify the delegate of the change. Asserting count value is easy, as just performing an equality check is enough. @testable import Tracker import XCTest

final class CounterTests: XCTestCase {

private var counter: Counter!

override func setUp() {
    super.setUp()
    counter = Counter()
}

func testIncrement() {
    counter.increment()

    XCTAssertEqual(countercount, 1)
}


However, it is a bit trickier to test calling the delegate. To keep track of and assert function calls, we need to assign a mock object on the delegate. As Swift is a static-typed language, concrete implementation of the mock is required to compile. Manually writing all of those properties and functions is error-prone and time consuming, so we need software helping us on the task. Tools like Sourcery can be used to generate these mock objects efficiently.

Understanding Mock Objects


Mock objects are a crucial part of unit testing, allowing developers to isolate dependencies and test specific components of their code in isolation. In the context of Swift development, mock objects can be used to mimic the behavior of real objects, making it easier to test complex systems. By using mock objects, developers can ensure that their code behaves as expected, even in scenarios where dependencies are not available or are difficult to test. This isolation helps in identifying issues within the unit being tested without interference from external factors, leading to more reliable and maintainable code.

Introducing Sourcery with Sourcery


Sourcery generates boilerplate code for various purposes by parsing Swift files. One of the usage for it is to automatically create a mocking class. To produce CounterDelegateMock class for CounterDelegate:

  1. Install Sourcery with the instruction

  2. Download AutoMockable.stencil

  3. Mark protocol auto-mockable by adding a comment (We can also make it to confirm AutoMockable protocol)

// sourcery: AutoMockable
protocol CounterDelegate AnyObject {

    func counter(_ counter: Counter, didUpdateCount count: Int)
}


Executing the following command outputs a file named AutoMockable.generated.swift containing CounterDelegateMock:

sourcery --sources <sources path> --templates <templates path> --output <output path>

class CounterDelegateMock CounterDelegate { //MARK: - counter var counterDidUpdateCountCallsCount = 0 var counterDidUpdateCountCalled: Bool { return counterDidUpdateCountCallsCount > 0 } var counterDidUpdateCountReceivedArguments: (counter: Counter, count: Int)? var counterDidUpdateCountReceivedInvocations: (counter: Counter, count: Int) = [] var counterDidUpdateCountClosure: ((Counter, Int) - Void)? func counter(_ counter: Counter, didUpdateCount count: Int) { counterDidUpdateCountCallsCount += 1 counterDidUpdateCountReceivedArguments = (counter: counter, count: count) counterDidUpdateCountReceivedInvocations.append((counter: counter, count: count)) counterDidUpdateCountClosure?(counter, count) } }

It implements detailed mocking functions automatically, make it possible to write sophisticated testing cases. Besides asserting if the function is called or not, checking the number of the function calls and arguments received can be useful for catching bugs or prevent regressions.

@testable import Tracker
import XCTest

final class CounterTests: XCTestCase {

    private var delegate: CounterDelegateMock!
    private var counter: Counter!

    override func setUp() {
        super.setUp()
        delegate = CounterDelegateMock()
        counter = Counter()
        counter.delegate = delegate
    }

    func testIncrement() {
        counter.increment()

        XCTAssertEqual(counter.count, 1)
        // Assert counter(_:, didUpdateCount:) is called
        XCTAssertTrue(delegate.counterDidUpdateCountCalled)
        // Assert received count parameter to be 1
        XCTAssertEqual(delegate.counterDidUpdateCountReceivedArguments?.count, 1)
    }
}


Now we have a complete set of class, delegate protocol, and mock delegate class consisting of a well-tested unit. During the process we only focused on actual business logic and use benefits from automatically generated code which is more than we wrote manually. This means increased productivity for better software.

Advantages of Using Sourcery for Mocking Class Generation


  1. Saving time by reducing repetitive typing: Mocking functions usually have a similar shape, call count, received arguments and return value. Therefore, it is better to automate such consistent and repetitive tasks.

  2. Easier maintenance: Mocks can be generated with a command very quickly. A command is enough after refactoring to keep the mocking class up-to-date.

  3. Better software architecture: Easier and more unit testing pushes us to write better software which is broken down into smaller and well-tested components.

Limitation of Sourcery for Mocking Class Generation1. Does not support all kinds of functions: Functions having generic types are not compatible with Sourcery.

2. Cannot distinguish overloaded functions: As Sourcery strips out parameter names, overloaded functions cause duplicated symbol issue. 3. Manually importing modules: Currently modules imported should be done manually as Sourcery only parses, not analyze, the code.

Auto-Generating Mock Classes


Auto-generating mock classes is a powerful feature of Sourcery, allowing developers to save time and effort when creating mock objects. By using Sourcery’s AutoMockable template, developers can automatically generate mock classes for their protocols, making it easier to write comprehensive unit tests. This feature is particularly useful when working with complex systems, where manual creation of mock objects can be time-consuming and error-prone. With Sourcery, you can focus on the actual business logic and let the tool handle the repetitive and tedious task of writing mock classes, ensuring consistency and reducing the likelihood of human error.

Integrating Mocks into Unit Tests


Integrating mocks into unit tests is a straightforward process, especially when using Sourcery’s AutoMockable template. By following a few simple steps, developers can easily create and use mock objects in their unit tests, ensuring that their code behaves as expected. This includes setting up the mock object, configuring its behavior, and verifying that the correct methods are called. For instance, after generating the mock class, you can assign it to the delegate property and use it to assert that specific methods are invoked with the expected arguments. This approach not only validates the functionality but also ensures that the interactions between components are correctly handled.

Best Practices and Resources


When working with mock objects and Sourcery, there are several best practices to keep in mind. These include:

  • Keeping mock objects simple and focused on specific behaviors: Avoid overcomplicating your mocks. They should only replicate the necessary behavior needed for the test.

  • Using Sourcery’s AutoMockable template to auto-generate mock classes: Leverage the power of Sourcery to automate the creation of mocks, ensuring consistency and saving time.

  • Testing mock objects thoroughly to ensure they behave as expected: Even though mocks are generated, it is essential to verify that they function correctly within your tests.

  • Using resources such as the Sourcery documentation and community forums to stay up-to-date with the latest features and best practices: Engage with the community and utilize available resources to enhance your understanding and usage of Sourcery.

Some recommended resources for learning more about mock objects and Sourcery include:

  • The Sourcery documentation and GitHub repository

  • The Swift community forums and Stack Overflow

  • Tutorials and guides on using Sourcery and mock objects in Swift development

Conclusion


In conclusion, mock objects are a powerful tool for unit testing in Swift development, and Sourcery’s AutoMockable template makes it easy to auto-generate mock classes. By understanding how to use mock objects and integrating them into unit tests, developers can ensure that their code behaves as expected and is thoroughly tested. By following best practices and staying up-to-date with the latest resources and features, developers can take their unit testing to the next level and deliver high-quality software. Embracing these tools and techniques not only enhances productivity but also contributes to building robust and maintainable applications.

Like 6 likes
Jason Nam
Share:

Join the conversation

This will be shown public
All comments are moderated

Get our stories delivered

From us to your inbox weekly.