Hello! If this is your first time reading one of my articles, my name is Skander. I’m an iOS developer passionate about Apple’s history. Here, I regularly share my knowledge with you. Instead of repeating content you can find millions of times elsewhere (like writing a for loop), I always aim to provide you with high-quality, valuable insights.

Each time I write an article, I’m usually on a train, which I find to be the perfect environment to write while admiring the beautiful French countryside. Today, I’m taking you along with me on my journey from Paris Montparnasse to Bordeaux, returning from SwiftConnection, held on September 23rd and 24th at the Théâtre de Paris.

I believe many developers, like myself, get bored writing Mocks or other repetitive code. While it can be frustrating, it’s essential for simulating coherent test scenarios. Fortunately, I’ve found a solution that will save you time — but it’s up to you to tailor it to your specific needs.

Step 1: Creating a Swift Package

To start, create a folder named MocksGenerator, or any name you prefer. Right-click on the folder and select “New Terminal at Folder,” then run the following command:

swift package init

This will generate a basic Swift package structure. You should see the following result:

Creating library package: MocksGenerator
Creating Package.swift
Creating .gitignore
Creating Sources/
Creating Sources/MocksGenerator/MocksGenerator.swift
Creating Tests/
Creating Tests/MocksGeneratorTests/
Creating Tests/MocksGeneratorTests/MocksGeneratorTests.swift

Among these files and directories, feel free to delete .gitignore (optional) and everything under Tests, as we won’t need them for our executable target.

Step 2: Modifying the Target

At this point, I’ve simplified my project to only these files:

Package.swift
Sources/
Sources/MocksGenerator/MocksGenerator.swift

Now, open the Package.swift file and modify the target. We’re going to remove the default library target and replace it with an executable one.

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "MocksGenerator",
    platforms: [
        .macOS(.v11)
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
        .package(url: "https://github.com/stencilproject/Stencil.git", from: "0.15.1"),
        .package(url: "https://github.com/MarkCodable/MarkCodable.git", from: "0.6.9"),
    ],
    targets: [
        .executableTarget(
            name: "MocksGenerator",
            dependencies: [
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
                .product(name: "Stencil", package: "Stencil"),
                .product(name: "MarkCodable", package: "MarkCodable")
            ],
            resources: [
                .copy("Resources/Template.stencil")
            ]
        ),
    ]
)

Step 3: Adding Necessary Files

Here are the dependencies we’ll be using:

  • swift-argument-parser: for decoding command-line arguments.
  • Stencil: an open-source project for creating dynamic templates.
  • MarkCodable: an open-source library for decoding Markdown text into Swift objects.

Inside the Sources/MocksGenerator directory, create a new folder called Resources and, inside it, a blank file named Template.stencil. Be careful — you may need to manually remove the .swift extension if it gets added automatically.

Here’s an example of what the Template.stencil file should look like:

import Foundation
{% for event in events -%}
import {{ event.moduleName }}

class {{ event.className }}Mock {
    {% for variable in event.variables %}
    let {{ variable }}: String
    {%- endfor %}
}

extension {{ event.className }}Mock: {{ event.className }}Protocol {

}
{% endfor %}

All the parameters for the events should come from a file named Events.md, which you’ll create at the root of your module.

Step 4: Creating the Events.md File

The Events.md file will serve as the source for our generator — a sort of local database if you will. It should look something like this:

| moduleName    | className        | variables                |
|---------------|------------------|--------------------------|
| Contact       | ContactViewModel | firstname,lastname,email |

This creates a Markdown table that acts as a separator, making it easy for both the algorithm to decode the fields and for humans to manipulate them.

Step 5: Final Project Structure

By this point, your file structure should look like this:

.
├── Package.swift
└── Sources
    ├── Events.md
    └── MocksGenerator
        ├── MocksGenerator.swift
        ├── Resources
        │   └── Template.stencil

Step 6: Let’s Get to Work!

Now, let’s move into the actual code! Before diving in, I need to relocate — there’s a gentleman snoring right next to me…

For this project, we’ll need four files.

  1. The Model
    The model will help us decode the data we pull from Events.md so we can manipulate it. Much like the models we create for JSON deserialization, this model should match the inputs defined in the Markdown table. In this example, our model would look like this:
import Foundation

struct Model: Decodable {
    let moduleName: String
    let className: String
    let variables: [String]

    private enum CodingKeys: String, CodingKey {
        case moduleName
        case className
        case variables
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        moduleName = try container.decode(String.self, forKey: .moduleName)
        className = try container.decode(String.self, forKey: .className)
        variables = try container.decode(String.self, forKey: .variables)
            .components(separatedBy: ",")
    }
}
  1. Helper Extension
    In a separate Swift file, or wherever you prefer, create an extension that allows us to decode the data into a String, with an optional .utf8 encoding:
import Foundation

extension URL {
    func loadString() throws -> String {
        let data = try Data(contentsOf: self)
        guard let string = String(data: data, encoding: .utf8) else {
            fatalError("Le fichier \(self) n'est pas un fichier texte valide.")
        }
        return string
    }
}
  1. MocksGenerator
    In MocksGenerator.swift, we’ll develop the logic to decode and encode data using the command-line arguments we parse.
import ArgumentParser
import Foundation

@main
struct MocksGenerator: ParsableCommand {
    @Option(name: .shortAndLong)
    var input: String
    
    @Option(name: .shortAndLong)
    var output: String = "GeneratedMocks.swift"
}

As you might notice, I’ve set the output class name as a string. You can change it or make it dynamic if needed, but the output file must exist for our argument to work.

  1. Loading Classes from Events.md

The first function in our struct will be called loadClasses. The idea here is to use MarkCodable to retrieve all the classes recorded in our Markdown table and then decode them into a [Model] array.

import ArgumentParser
import Foundation
import MarkCodable

@main
struct MocksGenerator: ParsableCommand {
    @Option(name: .shortAndLong)
    var input: String
    
    @Option(name: .shortAndLong)
    var output: String = "GeneratedMocks.swift"
    
    func loadClasses() throws -> [Model] {
        let inputPath = FileManager.default.currentDirectoryPath + "/Sources/" + input
        let items = try URL(fileURLWithPath: inputPath).loadString()

        return try MarkDecoder().decode([Model].self, from: items)
    }
}
  1. Generating Code from the Template

Once the classes are loaded and decoded into a [Model] array, we need to generate the code into the GeneratedMocks.swift file according to the Stencil template we created earlier.

The generate function will take the array of models, which we retrieve via loadClasses(). Then, we load the template file from the resources directory and, using the Stencil module, we generate a pre-filled template.

import ArgumentParser
import Foundation
import MarkCodable
import Stencil

@main
struct MocksGenerator: ParsableCommand {
    @Option(name: .shortAndLong)
    var input: String
    
    @Option(name: .shortAndLong)
    var output: String = "GeneratedMocks.swift"
    
    func loadClasses() throws -> [Model] {
        let inputPath = FileManager.default.currentDirectoryPath + "/Sources/" + input
        let items = try URL(fileURLWithPath: inputPath).loadString()

        return try MarkDecoder().decode([Model].self, from: items)
    }
    
    func generate(using classes: [Model]) throws -> String {
        guard let templateURL = Bundle.module
            .url(forResource: "Template", withExtension: "stencil") else {
            fatalError("Template non trouvé")
        }
        
        return try Environment().renderTemplate(
            string: templateURL.loadString(),
            context: ["events": classes]
        )
    }
}
  1. Final Execution

Finally, we have the run function, which will be executed from the command line. Here’s what it does:

  1. It loads the classes using loadClasses().
  2. It generates the corresponding code from the template.
  3. It writes that code into the GeneratedMocks.swift file.
import ArgumentParser
import Foundation
import MarkCodable
import Stencil

@main
struct MocksGenerator: ParsableCommand {
    @Option(name: .shortAndLong)
    var input: String
    
    @Option(name: .shortAndLong)
    var output: String = "GeneratedMocks.swift"
    
    func loadClasses() throws -> [Model] {
        let inputPath = FileManager.default.currentDirectoryPath + "/Sources/" + input
        let items = try URL(fileURLWithPath: inputPath).loadString()

        return try MarkDecoder().decode([Model].self, from: items)
    }
    
    func generate(using classes: [Model]) throws -> String {
        guard let templateURL = Bundle.module
            .url(forResource: "Template", withExtension: "stencil") else {
            fatalError("Template non trouvé")
        }
        
        return try Environment().renderTemplate(
            string: templateURL.loadString(),
            context: ["events": classes]
        )
    }
    
    mutating func run() throws {
        let classes = try loadClasses()
        let generatedCode = try generate(using: classes)

        // Écrire le fichier de sortie sur le disque
        let outputPath = FileManager.default.currentDirectoryPath + "/" + output
        try generatedCode.write(
            to: URL(fileURLWithPath: outputPath),
            atomically: true,
            encoding: .utf8
        )
    }
}
  1. Final Result

Your file structure should now look like this:

.
├── Package.resolved
├── Package.swift
└── Sources
    ├── Events.md
    └── MocksGenerator
        ├── GeneratedMocks.swift
        ├── MocksGenerator.swift
        ├── Model.swift
        ├── Resources
        │   └── Template.stencil
        └── URL+Extension.swift
  1. Running the Command

From the same root directory, run the following command in your terminal:

swift run MocksGenerator --input Events.md

The magic happens in the GeneratedMocks.swift file, where you’ll see the following generated code:

import Foundation
import Contact

class ContactViewModelMock {
    let firstname: String
    let lastname: String
    let email: String
}

extension ContactViewModelMock: ContactViewModelProtocol {

}

Conclusion

Congratulations! You now have an automated mock generator in Swift that can save you valuable time when writing unit tests. Feel free to adapt this solution to your specific needs, and watch how it boosts your productivity in no time.

Categorized in:

Swift,