"Test First — Code Later": Why It Works in Swift?
\n\nHave you ever rewritten the same module for the third time because "it needed to be shipped urgently"? Or caught a bug in production that could have been easily prevented during the writing phase? If so — welcome to the world of TDD (Test-Driven Development).
\n\nTest-driven development is not just a trendy methodology. It's a way of thinking that forces you to formulate how the code should behave before you write it. In Swift, with its strong typing and powerful XCTest framework, TDD becomes a reliable shield against regressions and unexpected crashes.
In this article, we'll break down TDD with real-world examples, write tests for the networking layer and data model, and most importantly — understand how to implement this practice without pain. Let's go!
\n\nRed-Green-Refactor: The Magic of the Cycle
\n\nThe foundation of TDD is a three-step cycle:
\n\n- \n
- Red — write a test that fails (because the code doesn't exist yet). \n
- Green — write the minimum code to make the test pass. \n
- Refactor — improve the code without changing its behavior (tests protect you). \n
Seems simple? Let's try it in Swift.
\n\nFirst Test: Checking the User Model
\n\nLet's say we have a simple user model. We want it to correctly format the full name. First — the test:
\n\nimport XCTest\n@testable import MyApp\n\nfinal class UserTests: XCTestCase {\n \n func testFullName_GivenFirstNameAndLastName_ReturnsFormattedString() {\n // Arrange\n let user = User(firstName: "Anna", lastName: "Karenina")\n \n // Act\n let fullName = user.fullName()\n \n // Assert\n XCTAssertEqual(fullName, "Anna Karenina", "Full name should consist of first and last name separated by a space")\n }\n}\n\n\nRun it — the test fails (red). The compiler complains that User doesn't exist. Perfect, that's the intention.
Now write the minimum code to pass the test:
\n\nstruct User {\n let firstName: String\n let lastName: String\n \n func fullName() -> String {\n return "\\(firstName) \\(lastName)"\n }\n}\n\n\nRun it — green! Now you can move on to refactoring. For example, add handling for empty strings or a middle name. But don't rush — the tests will tell you when it's needed.
\n\nTesting the Asynchronous Networking Layer
\n\nOne of the most common problems in iOS development is working with the network. TDD is especially useful here because you explicitly separate logic from network calls.
\n\nWriting a Protocol and a Mock Object
\n\nFirst, let's define what our service should look like:
\n\nprotocol WeatherServiceProtocol {\n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void)\n}\n\n\nNow — the test. We don't want to depend on a real API, so we create a mock:
\n\nclass MockWeatherService: WeatherServiceProtocol {\n var shouldReturnError = false\n var mockWeather: Weather?\n \n func fetchWeather(for city: String, completion: @escaping (Result<Weather, Error>) -> Void) {\n if shouldReturnError {\n completion(.failure(NetworkError.serverError))\n } else if let weather = mockWeather {\n completion(.success(weather))\n }\n }\n}\n\nfinal class WeatherViewModelTests: XCTestCase {\n \n func testFetchWeather_Success_UpdatesModel() {\n // Arrange\n let mockService = MockWeatherService()\n mockService.mockWeather = Weather(temperature: 25.0, condition: "sunny")\n let viewModel = WeatherViewModel(service: mockService)\n let expectation = XCTestExpectation(description: "Waiting for weather data to load")\n \n