Skip to content

A powerful Swift CLI tool that automatically generates mock objects (stubs, spies, and dummies) from your Swift source code using comment annotations.

License

Notifications You must be signed in to change notification settings

manucodin/SwiftMockGenerator

Repository files navigation

SwiftMockGenerator

codecov Swift macOS License

A powerful Swift CLI tool that automatically generates comprehensive mock objects (stubs, spies, and dummies) from your Swift source code using simple comment annotations. Perfect for unit testing, TDD, and creating reliable test doubles.

✨ Features

  • 🎯 Comment-based Generation: Generate mocks using simple annotations like // @Stub, // @Spy, // @Dummy
  • πŸš€ Zero Runtime Dependencies: Generated mocks have no dependencies on the generator tool
  • πŸ”§ Multiple Mock Types: Supports stubs, spies, and dummy implementations
  • ⚑ Swift Syntax Powered: Uses Apple's SwiftSyntax for accurate Swift code parsing
  • πŸ› οΈ CLI Interface: Easy to integrate into build processes and CI/CD pipelines
  • πŸ“Š Call Tracking: Spies automatically track method calls, parameters, and return values
  • 🎭 Error Mocking: Configure spies to throw specific errors for testing error scenarios
  • πŸ“ Clean Code: Generated mocks follow Swift best practices with organized structure
  • πŸ” Verbose Logging: Detailed output for debugging and monitoring
  • πŸ”„ Async Result Support: Generate mocks with Result<T, Error> for async methods using --use-result
  • πŸ”’ Sendable Support: Automatically detects and marks mocks as @unchecked Sendable when needed

πŸš€ Quick Start

Installation

Manually

# Clone the repository
git clone https://github.com/manucodin/SwiftMockGenerator.git
cd SwiftMockGenerator

# Install system-wide
make install

# Or run directly
swift run swift-mock-generator --input ./Sources --output ./Tests/Mocks

🌱 Mint

mint install manucodin/SwiftMockGenerator

Basic Usage

  1. Annotate your code with mock comments:
// @Stub
protocol NetworkService {
    func fetchData() async throws -> Data
    var isConnected: Bool { get }
}

// @Spy
class DataManager {
    func save(data: String) throws -> Bool {
        return true
    }
}
  1. Generate mocks:
# Standard generation
swift-mock-generator --input ./Sources --output ./Tests/Mocks --verbose

# With Result<T, Error> for async methods
swift-mock-generator --input ./Sources --output ./Tests/Mocks --use-result --verbose
  1. Use in your tests:
func testDataManager() throws {
    let spy = DataManagerSpy()
    spy.saveReturnValue = true
    spy.saveThrowError = NetworkError.connectionFailed
    
    // Test your code with the spy
    XCTAssertThrowsError(try spy.save(data: "test"))
    XCTAssertEqual(spy.saveCallCount, 1)
}

πŸ“– Command Line Options

Option Short Description Default
--input -i Input directory containing Swift source files .
--output -o Output directory for generated mock files ./Mocks
--verbose -v Enable verbose logging false
--clean Clean output directory before generating mocks false
--use-result Use Result<T, Error> for async methods instead of async throws false

πŸ”„ Async Result Support

When working with async methods, you can choose between two approaches:

Standard Async (Default)

// @Stub
protocol NetworkService {
    func fetchData() async throws -> Data
}

Generated Mock:

class NetworkServiceStub: NetworkService {
    func fetchData() async throws -> Data {
        return Data()
    }
}

Result-based Async (--use-result)

swift-mock-generator --input ./Sources --output ./Tests/Mocks --use-result

Generated Mock:

class NetworkServiceStub: NetworkService {
    var fetchDataReturnValue: Result<Data, Error> = .success(Data())
    
    func fetchData() async throws -> Data {
        return try fetchDataReturnValue.get()
    }
}

Usage in Tests:

func testNetworkService() async throws {
    let stub = NetworkServiceStub()
    
    // Simulate success
    stub.fetchDataReturnValue = .success(sampleData)
    
    // Simulate error
    stub.fetchDataReturnValue = .failure(NetworkError.timeout)
    
    let result = try await stub.fetchData()
    // Handle the unwrapped Data value
}

πŸ”’ Sendable Support

The tool automatically detects when protocols or classes conform to Sendable and marks the generated mocks accordingly:

// @Stub
protocol SendableService: Sendable {
    func fetchData() async throws -> String
}

Generated Mock:

@unchecked Sendable class SendableServiceStub: SendableService, Sendable {
    var fetchDataReturnValue: Result<String, Error> = .success("")
    
    func fetchData() async throws -> String {
        return try fetchDataReturnValue.get()
    }
}

This ensures your mocks are safe to use in concurrent contexts when the original type is Sendable.

🎭 Mock Types

Stub (// @Stub)

Generates implementations with sensible default return values:

// @Stub
protocol UserService {
    func getUser(id: String) async throws -> User
    var isLoggedIn: Bool { get }
}

Generated Stub:

class UserServiceStub: UserService {
    var getUserReturnValue: User = User()
    var isLoggedInReturnValue: Bool = false
    
    func getUser(id: String) async throws -> User {
        return getUserReturnValue
    }
    
    var isLoggedIn: Bool {
        return isLoggedInReturnValue
    }
}

Spy (// @Spy)

Generates implementations that record method calls and parameters:

// @Spy
class DataRepository {
    func save(_ item: Item) throws -> Bool {
        return true
    }
    
    func load(id: String) -> Item? {
        return nil
    }
}

Generated Spy:

class DataRepositorySpy: DataRepository {
    
    // MARK: - Reset
    func resetSpy() {
        saveCallCount = 0
        saveCallParameters = []
        saveThrowError = nil
        loadCallCount = 0
        loadCallParameters = []
        loadReturnValue = nil
    }
    
    // MARK: - save
    private(set) var saveCallCount = 0
    private(set) var saveCallParameters: [(Item)] = []
    var saveThrowError: Error?
    var saveReturnValue: Bool = false
    
    func save(_ item: Item) throws -> Bool {
        saveCallCount += 1
        saveCallParameters.append((item))
        if let error = saveThrowError { throw error }
        return saveReturnValue
    }
    
    // MARK: - load
    private(set) var loadCallCount = 0
    private(set) var loadCallParameters: [(String)] = []
    var loadReturnValue: Item?
    
    func load(id: String) -> Item? {
        loadCallCount += 1
        loadCallParameters.append((id))
        return loadReturnValue
    }
}

Dummy (// @Dummy)

Generates minimal implementations that satisfy compile-time requirements:

// @Dummy
protocol Logger {
    func log(message: String)
    func logError(_ error: Error)
}

Generated Dummy:

class LoggerDummy: Logger {
    func log(message: String) {
        // Dummy implementation - does nothing
    }
    
    func logError(_ error: Error) {
        // Dummy implementation - does nothing
    }
}

πŸ› οΈ Makefile Commands

make help          # Show all available commands
make install       # Build and install system-wide
make uninstall     # Remove from system
make test          # Run test suite (99 tests)
make coverage      # Run tests with coverage report
make demo          # See the tool in action with examples
make clean         # Clean build artifacts

🎯 Supported Swift Features

  • βœ… Protocols with methods, properties, and inheritance
  • βœ… Classes with inheritance and final modifiers
  • βœ… Functions with parameters, return types, and async/await
  • βœ… Generic types and functions
  • βœ… Throwing functions with error mocking
  • βœ… Access control levels (public, internal, private, fileprivate)
  • βœ… Property wrappers and computed properties
  • βœ… Initializers with various modifiers
  • βœ… Sendable conformance with automatic @unchecked Sendable marking
  • βœ… Async/await with optional Result<T, Error> support

πŸ”§ Integration

Running from Xcode

The project can be opened and executed directly from Xcode for development and debugging purposes.

Opening the Project

  1. Open Xcode
  2. File β†’ Open
  3. Select the Package.swift file in the project root
  4. Click "Open"

Configuring the Execution Scheme

To run the project with custom arguments:

  1. Product β†’ Scheme β†’ Edit Scheme...
  2. In the "Run" tab:
    • Ensure the Executable is set to SwiftMockGenerator
  3. In the "Arguments" tab:
    • Add command line arguments as needed:
      • --input "./Examples/Sources" (input directory)
      • --output "./Examples/Mocks" (output directory)
      • --verbose (enable detailed logging)
      • --clean (clean output directory before generation)
      • --module "MyModule" (specify module name for @testable import)
      • --use-result (use Result<T, Error> for async methods)

Running the Project

  1. Press Cmd+R to build and run
  2. Or use Product β†’ Run from the menu
  3. Check the console output for generation results

Xcode Build Phase

Add a build phase script to automatically generate mocks:

if which swift-mock-generator > /dev/null; then
  swift-mock-generator --input ./Sources --output ./Tests/Mocks --verbose
else
  echo "SwiftMockGenerator not found. Installing..."
  # Add installation commands here
  exit 1
fi

Swift Package Manager

Add as a dependency in your Package.swift:

dependencies: [
    .package(url: "https://github.com/manucodin/SwiftMockGenerator.git", from: "1.0.0")
]

πŸ› Troubleshooting

Common Issues

  1. Compilation Errors: Ensure your input Swift files are syntactically correct
  2. No Mocks Generated: Check that comment annotations are properly formatted
  3. Access Level Issues: Generated mocks respect the access levels of original types
  4. Missing Dependencies: Ensure Swift 5.9+ and macOS 12+ are installed
  5. Sendable Warnings: Use @unchecked Sendable for mocks that need to be Sendable but have mutable state
  6. Async Result Issues: Use --use-result flag when you need Result<T, Error> instead of async throws

Debug Mode

Use --verbose flag to see detailed logging:

swift-mock-generator --input ./Sources --output ./Tests/Mocks --verbose

Getting Help

  • Check the Examples directory for sample usage
  • Run make demo to see the tool in action
  • Review the test suite for implementation patterns

πŸ“‹ Requirements

  • Swift: 5.9+
  • macOS: 12+
  • Xcode: 14+ (for development)

πŸ“¦ Dependencies

🀝 Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes
  4. Add tests for new functionality
  5. Ensure all tests pass (make test)
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments


Made with ❀️ for the Swift community

About

A powerful Swift CLI tool that automatically generates mock objects (stubs, spies, and dummies) from your Swift source code using comment annotations.

Resources

License

Stars

Watchers

Forks

Packages

No packages published