> ## Content Index
> Fetch the complete content index at: https://williamboles.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Hitting the Target with TestHelpers
- URL: https://williamboles.com/hitting-the-target-with-testhelpers/
- Published: 2025-07-15T08:49:06.000Z
- Updated: 2026-09-26T20:44:23.000Z
- Description: Modules are super useful for splitting our projects into smaller, more manageable parts. But they could be doing more. Here, we look at how a utility module can ease the burden not only for production targets but also for test targets.
- Author: William Boles
- Tags: Modularisation, Xcode, Testing

As a project grows, [modularisation](https://en.wikipedia.org/wiki/Modular%5Fprogramming?ref=williamboles.com) becomes a valuable tool for managing that growth. In the projects I've worked on, the benefits of modularisation are almost exclusively felt in the production target, with little benefit being felt in the test target. This disparity in benefits doesn't need to exist - modularisation should benefit **both** production and test targets. All we need to do is realise that there are more than just tests in a test target.

![A photo of an arrow impaled in a target](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/photo-of-arrow-in-a-target-min.jpg)

This post will examine how to organise a project to ensure that modularisation reduces complexity in both production and test targets by introducing test-helper targets to each module.

> If you are struggling to identify modules in your existing project, see [Leveraging Single-Responsibility Principle to Find Hidden Modules](https://williamboles.com/leveraging-single-responsibility-principle-to-find-hidden-modules/) on ways to find them.

## Lining up the target 🏹

Once we start modularising a project, we quickly discover that not all modules are the same. Some modules are *feature* modules, designed to be consumed by the end user; others are *utility* modules, designed to be consumed by other modules. When a 3rd-party module uses a utility module, the developer will add that utility module as a dependency of the 3rd-party module. The functionality of the utility module is then consumed as-is. However, when that same developer adds tests to the 3rd-party module, they often have to write [test doubles](https://en.wikipedia.org/wiki/Test%5Fdouble?ref=williamboles.com) of the utility module's public functionality to test their module's functionality, as the utility module doesn't provide those test doubles. Suppose that the utility module is a dependency for multiple other modules - each of those other modules now has to either:

1. Rewrite those same test doubles leading to duplicated work.
2. Introduce a test double module for each utility module, leading to a lot more module management being required.
3. Introduce a common shared test double module leading to different domains in the project artificially living together (i.e. no production reason) so unintentionally coupling those otherwise independent modules together.

So, we either waste development time reinventing something that already exists, or we increase project complexity with unnecessary modules or we couple modules together when there is no production value in it.

### 🤮

A better approach is to have each utility module produce a test-helper target containing the test doubles of that module's **public** interface. Any 3rd-party test target can then consume that utility module's public test double functionality in the same way as its production target can consume that utility module's public production functionality.

A dedicated test-helper target for each utility module has the following advantages:

1. **Eliminates duplication of test doubles** \- rather than each 3rd-party module having to implement its own test double, those test doubles are grouped and exposed via the test-helper target of the module.
2. **Improves discoverability by creating a source-of-truth** \- having a single source-of-truth for each module's test doubles means that everyone knows where to get those test doubles. If the required test double does not exist, then they can fill in the gap, helping future consumers of that utility module.
3. **Improves consistency of each test double type** \- grouping the test doubles makes it easier to spot inconsistencies.
4. **Improves portability** \- the module provides a public test interface, allowing any 3rd-party module to more quickly integrate the utility module into both production and test targets, with production and test double functionality being consumed as-is.

Let's add a test-helper target to an example app.

## Adding a test-helper target

Checkout the [example app](https://github.com/wibosco/TestHelpers-Example/?ref=williamboles.com) to follow along.

The example app has a workspace containing two projects:

1. `TestHelpers-Example` \- the app project.
2. `Networking` \- a utility framework project.

`TestHelpers-Example` depends on `Networking`.

Currently, `Networking` lacks a test-helper target to make writing unit tests in `TestHelpers-Example` easier; let's change that.

1. Open the `Networking` target list and add a new target by clicking on the `+` symbol:

![Screenshot of adding a new target](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-adding-new-target.jpg)

1. On the `Choose a template for your new target` window, scroll down and select `Framework`:

![Screenshot of selecting framework template](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-selecting-framework.jpg)

1. On the `Choose options for your new target` window:  
3.1\. Give the new target a product-name of `NetworkingTestHelpers`.  
3.2\. Select `None` in the `Testing System` dropdown list.

![Screenshot of naming new target](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-naming-test-helper-target.jpg)

> I normally suffix `TestHelpers` to the project name to get the test-helper target name.

Following those steps should add a framework target to the project.

1. All that is left to do now is to add the production target as a dependency on the new test-helpers target:

![Screenshot of adding the production target as a dependency of the TestHelper target](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-adding-networking-framework-to-test-helper-target.jpg)

Now that you have a test-helper target, let's populate it.

## Adding test doubles

The test-helper target contains the public test doubles for that utility module. The public test doubles are only those types with access control levels of `open` or `public`.

`Networking` only has one protocol and one concrete type: `NetworkingService` and `DefaultNetworkingService`. `DefaultNetworkingService` is a concrete type and, as such, is the concern of the production code; therefore, we can ignore it. Instead, let's focus on the protocol - `NetworkingService`:

```
public protocol NetworkingService {
    func makeRequest(url: URL) async -> Data?
}
```

A stub test double of `NetworkingService` might look like:

```
// 1
import Networking

// 2
public class StubNetworkingService: NetworkingService {
    public enum Event: Equatable {
        case makeRequestCalled(URL)
    }

    private(set) public var events = [Event]()

    public var dataToBeReturned: Data?

    public init() { }

    public func makeRequest(url: URL) async -> Data? {
        events.append(.makeRequestCalled(url))

        return dataToBeReturned
    }
}
```

> This isn't a post about how to write test doubles, so there is no need to get in touch about how you consider `StubNetworkingService` to be more of a `Spy` than a `Stub`.

Here's what we did:

1. Imported the `Networking` production target using a standard `import`. When adding a test double to the test-helpers target, avoid using `@testable import`, as the test-helpers target should have the same constraints as any other module when dealing with the production target. We don't want to accidentally break the encapsulation of `Networking` via its test doubles.
2. Created a `public` test double.

Now that we have a test-helper target and a test double, let's use them.

## Using the test-helper target

In `TestHelpers-Example`, the type `AwesomeFeature` uses `NetworkingService`. To ensure that awesomeness continues, we need to write unit tests. Thankfully, `TestHelpers-Example` already has a unit test target. However, that target doesn't depend upon our new test-helpers target, so open `TestHelpers-ExampleTests` in the target list and add `NetworkingTestHelpers` as a dependency:

![Screenshot of adding TestHelper target as dependency](https://storage.ghost.io/c/b9/b4/b9b42214-5933-4584-8e56-9ee8ca21d23b/content/images/size/w1000/2025/04/Screenshot-adding-test-helper-framework-to-test-target.jpg)

Open `AwesomeFeatureTests` and add `NetworkingTestHelpers` as an import:

```
import NetworkingTestHelpers
```

Again, note that we don't use `@testable import` here.

We can now use `StubNetworkingService` to write any unit tests as we would with any other imported type:

```
struct AwesomeFeatureTests {

    // MARK: - Tests

    @Test("Given gainAwesomeness is called, then networking service is called with the passed in URL")
    func checkNetworkRequestIsMade() async {
        
        // 1
        let networkingService = StubNetworkingService()
        let url = URL(string: "https://example.com")!
        let sut = AwesomeFeature(networkingService: networkingService)

        await sut.gainAwesomeness(from: url)

        #expect(networkingService.events == [.makeRequestCalled(url)])
    }
}
```

Here's what we did:

1. Used the `StubNetworkingService` declared in `NetworkingTestHelpers` to test whether the instance `AwesomeFeature` works as expected.

Nothing about the above test is unusual, except that we are using an imported test double.

If another module needs to verify that its networking calls are being made correctly. In that case, importing `NetworkingTestHelpers` will enable the other test module to access the functionality of `StubNetworkingService` in much the same way as its production target accesses `NetworkingService`.

## Bullseye 🎯

This post provides a straightforward example of using a module's test target to share a test double. While the example might be simple, it illustrates how this approach can effectively scale to share any necessary test double while adhering to the principles of modularisation.

Writing effective unit tests is crucial for any app, but the process can be time-consuming. By utilising test-helper targets, we can maximise the efficiency of our unit testing efforts. This approach eliminates duplicated work, enhances discoverability, improves consistency, and increases the portability of each module.

To see the completed project with a test-helper target, checkout the [NetworkingTestHelpers](https://github.com/wibosco/TestHelpers-Example/tree/NetworkingTestHelpers?ref=williamboles.com) branch.