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 termFakes
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
- “Fewer, Smarter, Faster: Scaling Testing @Spotify” by Vivian Santos & Sami Bouchebaba This talk is the where I’ve found this idea. See the video around 12:00 minute mark.
- TestDouble