After SPM asset support has been added to Xcode 12, there is no reason not to use modularization in your iOS projects. It brings value no matter the project and team size.

However, after some time, you might encounter problems like increased complexity, difficulty with integration testing, or code duplication. Today we’ll focus on the last one, specifically, testing doubles duplication. Let’s jump in!

Introduction

When saying “module” I’m always referring to a group of related targets. One example would be DomainModels and DomainModelsTests. The DomainModels target stores the implementation, while DomainModelsTests has unit tests code.

In the sample project for this article, we have the DomainModels package with a single module: DomainModels. Apart from that, we have the FeatureModules package with multiple modules. High-level structure of this setup looks like this:

├── DomainModels
│   ├── DomainModels
│   └── DomainModelsTests
└── FeatureModules
    ├── Dashboard
    ├── DashboardTests
    ├── AccountOverview
    └── AccountOverviewTests

All modules in the FeatureModules package depend on the DomainModels module. Because of that, both DashboardTests and AccountOverviewTests will likely have some duplicate testing doubles. This is not ideal.

There are many possible solutions to that problem. The one that I like the most is creating a separate target in the module just for storing all of its testing doubles. Let’s implement it!

Solution

The solution is fairly simple. We’ll just add the third target to the DomainModels module: DomainModelsFakes and move all the testing doubles from feature modules there.

Although the TestingDoubles postfix may be more appropriate for these modules, I prefer the term Fakes as it conveys the same meaning and is shorter. This choice becomes more practical when considering that there will likely be a significant number of these modules.

With this new addition, the DomainModels module consists of three targets:

  • DomainModels
  • DomainModelsTests
  • DomainModelsFakes

Here’s the package definition:

// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "DomainModels",
    platforms: [.iOS(.v16), .macOS(.v13)],
    products: [
        .library(
            name: "DomainModels",
            targets: ["DomainModels", "DomainModelsFakes"]),
    ],
    dependencies: [],
    targets: [
        .target(
            name: "DomainModels",
            dependencies: []),
        .target(
            name: "DomainModelsFakes",
            dependencies: ["DomainModels"]),
        .testTarget(
            name: "DomainModelsTests",
            dependencies: ["DomainModels", "DomainModelsFakes"]),
    ]
)

The new target DomainModelsFakes depends on the DomainModels target. We also need to add it as a dependency to the test target to use the testing doubles.

With those changes in place, we can now add DomainModelsFakes as a dependency to feature modules testing targets:

// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "FeatureModules",
    platforms: [.iOS(.v16), .macOS(.v13)],
    products: [
        .library(
            name: "FeatureModules",
            targets: ["Dashboard", "AccountOverview"]),
    ],
    dependencies: [
        .package(path: "../DomainModels"),
    ],
    targets: [
        .target(
            name: "Dashboard",
            dependencies: [
                .product(name: "DomainModels", package: "DomainModels"),
            ]),
        .testTarget(
            name: "DashboardTests",
            dependencies: [
                "Dashboard",
                .product(name: "DomainModelsFakes", package: "DomainModels"),
          ]),
        .target(
            name: "AccountOverview",
            dependencies: [
                .product(name: "DomainModels", package: "DomainModels"),
            ]),
        .testTarget(
            name: "AccountOverviewTests",
            dependencies: [
                "AccountOverview",
                .product(name: "DomainModelsFakes", package: "DomainModels"),
          ]),
    ]
)

Conclusion

As our project grows, we may encounter problems such as code duplication, which can lead to increased complexity and difficulty with integration testing. In this article, we focused on the issue of testing doubles duplication and provided a solution to address it by creating a separate target in the module to store all testing doubles. By implementing this solution, we can reduce code duplication and improve the maintainability of our codebase. Do let me know if there are any other solutions worth trying out!

Resources