Unit Testing in Swift Made Easy: A Beginner’s Guide With Real Examples
A beginner’s guide to Swift unit testing — learn the basics, write your first tests, and improve your code quality.

✍️ Introduction
Unit testing in Swift sounds serious… until you realize it’s just code that checks your code ✅
If you’ve ever deployed an app, crossed your fingers, and said “Please don’t crash,” this one’s for you. Writing unit tests might feel like a chore at first — but once you get the hang of it, they become your personal bug bouncers. No crash? No panic? No problem.
In this easy-to-follow guide, we’ll break down unit testing in Swift using real-world examples, minus the jargon and overwhelm. Whether you’re building your first feature or trying to cover an entire legacy mess (no judgment 😬), this article will help you get started confidently.
Let’s write tests that future-you will actually appreciate — no time machines needed 🧪🚀
🧪 Why Unit Testing Matters (Yes, Even for Small Projects)
If you’ve ever tried fixing a bug and ended up introducing three new ones, you’re not alone. It’s like playing Whac-A-Mole with your code 🐹🔨
Unit testing helps stop that chaos before it starts. Instead of waiting for a QA team (or worse, your users) to discover what’s broken, you let your tests do the catching for you. Every time you change something, your tests give you instant feedback — no guesswork, no crossed fingers, no late-night debugging sessions (hopefully).
But it’s not just about avoiding bugs. Unit tests make your code:
- Easier to refactor confidently
- Safer to collaborate on in a team
- More maintainable when your app starts growing faster than your coffee intake ☕️
And let’s not forget the big win: writing tests actually makes you a better developer. When your functions are testable, your architecture often gets cleaner — which leads to fewer tangled dependencies and mysterious side effects.
👉 If you want to design your code so it’s easier to test from the get-go, _understanding SOLID principles in Swift_ is a great next step.
**SOLID Principles in Swift Made Easy (With Real-Life Examples)** _A beginner-friendly guide to SOLID principles in Swift — packed with code examples and real-world use cases🤓🔥_medium.comhttps://medium.com/swift-pal/solid-principles-in-swift-made-easy-with-real-life-examples-9af053523e82
Ready to meet XCTest, your testing sidekick? 🦸♂️
🧰 What Is XCTest and How Does It Work?
Before you start writing tests, you need to meet your new BFF in the Swift world: XCTest.
Think of XCTest as the toolkit Apple gives you to test your Swift code. It’s built right into Xcode and lets you:
- Write tests that check your functions do what they’re supposed to
- Run all those tests with a single click (or keyboard shortcut ⌘U, anyone?)
- Get instant feedback if something breaks — no more silent errors sneaking into production
At its core, XCTest is just a bunch of functions that look like:
XCTAssertEqual(actualValue, expectedValue)That’s it! You’re telling Swift: “Yo, I expect this value to be 5. If it’s not, scream at me.”
⚙️ A Quick Look at the XCTest Lifecycle
Here’s what a basic test class looks like:
import XCTest
final class MathUtilsTests: XCTestCase {
override func setUp() {
super.setUp()
// Called before each test method — great for setup
}
override func tearDown() {
super.tearDown()
// Clean up after each test method
}
func testAddition() {
let result = MathUtils.add(2, 3)
XCTAssertEqual(result, 5)
}
}Let’s break it down:
- XCTestCase: A class that contains your test methods.
- setUp() / tearDown(): Optional methods for prepping and cleaning after each test. Think of them as opening and closing scenes in a play 🎭
- Test functions must start with test or Xcode won’t recognize them.
- XCTAssertEqual checks if your actual result matches what you expected.
And just like that, you’ve written your first test.
Ready to write a real one together? Let’s go 🚀
🧑💻 Writing Your First Unit Test (Step-by-Step)
Alright, you’ve met XCTest — now let’s put it to work. We’ll start with something simple and relatable: testing a tiny function that adds two numbers.
🧠 Step 1: Create a Simple Function to Test
Let’s say you have a Swift file like this in your main app target:
// MathUtils.swift
struct MathUtils {
static func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
}Looks innocent enough, right? Now let’s test it.
🧪 Step 2: Add a Test Target (If You Don’t Have One)
If your project doesn’t already have a test target, here’s how to add one:
- Go to your Xcode project settings.
- Click the + under Targets, then choose Unit Testing Bundle.
- Name it something like YourAppTests and make sure it’s linked to your main app target.
Now you should see a YourAppTests folder with a YourAppTests.swift file in it.
🧷 Step 3: Write Your First Test Case
Inside that test file, import your module and write your test:
import XCTest
@testable import YourApp
final class MathUtilsTests: XCTestCase {
func testAdd_twoPositiveNumbers_returnsCorrectSum() {
// Arrange
let a = 2
let b = 3
// Act
let result = MathUtils.add(a, b)
// Assert
XCTAssertEqual(result, 5)
}
}Let’s break it down, quick and clean:
- @testable import YourApp lets your test target access internal types and methods.
- The test function uses the Arrange–Act–Assert pattern: 1\. Arrange your inputs 2. Act by calling the function 3\. Assert the result is what you expect
Run the test with ⌘U, and boom — you’re officially testing in Swift 🥳
Want to test something more real-world next? Like a ViewModel or a network response? Let’s go there next 👇
🔍 Testing Real Code: ViewModels, Networking, and More
Toy examples are great… but your real code is rarely just 2 + 3. Let’s now look at how to test the kinds of logic you’ll actually have in a real app — like ViewModels, services, and even async networking code.
🧠 Example: Testing a ViewModel
Let’s say we have a very “startup MVP-ish” LoginViewModel:
// LoginViewModel.swift
final class LoginViewModel {
func isValid(email: String, password: String) -> Bool {
return email.contains("@") && password.count >= 8
}
}Now in your test target:
final class LoginViewModelTests: XCTestCase {
func testIsValid_withValidInputs_returnsTrue() {
let viewModel = LoginViewModel()
let isValid = viewModel.isValid(email: "test@email.com", password: "password123")
XCTAssertTrue(isValid)
}
func testIsValid_withInvalidEmail_returnsFalse() {
let viewModel = LoginViewModel()
let isValid = viewModel.isValid(email: "invalid", password: "password123")
XCTAssertFalse(isValid)
}
}✅ Easy, fast, and gives you confidence your logic won’t go rogue mid-sprint.
🌐 Example: Testing a Networking Layer
Let’s say your networking logic is abstracted in a protocol like this:
protocol AuthService {
func login(email: String, password: String, completion: @escaping (Bool) -> Void)
}Now, you want to test your logic without actually hitting the network. This is where mocks shine ✨
final class MockAuthService: AuthService {
var shouldSucceed = true
func login(email: String, password: String, completion: @escaping (Bool) -> Void) {
completion(shouldSucceed)
}
}And the ViewModel might look like:
final class LoginViewModel {
private let authService: AuthService
init(authService: AuthService) {
self.authService = authService
}
func login(email: String, password: String, completion: @escaping (Bool) -> Void) {
authService.login(email: email, password: password, completion: completion)
}
}This pattern of injecting mocks is exactly why good architecture makes testing so smooth 🧈
🎯 Want to see how to design your app so this kind of testing becomes second nature?
Check out _Understanding Clean Architecture in iOS_ and _How to Structure a Scalable iOS App With Modular Architecture_
**Understanding Clean Architecture in iOS: A Beginner’s Guide** _Tired of Spaghetti Code? 🍝 Let’s Clean Things Up in iOS!_medium.comhttps://medium.com/swift-pal/understanding-clean-architecture-in-ios-a-beginners-guide-69d09b4883c4
**How to Structure a Scalable iOS App with Modular Architecture** _Struggling with messy codebases and slow builds? Learn how modular architecture can make your iOS app scalable…_medium.comhttps://medium.com/swift-pal/how-to-structure-a-scalable-ios-app-with-modular-architecture-b0130da83bca
🛠️ Mocks, Stubs, and Fakes: Making Tests Actually Useful
So far, you’ve written tests for pure functions and even mocked a network service. But what happens when your code starts depending on everything — APIs, databases, time, weather, your manager’s mood? 🫣
That’s where mocks, stubs, and fakes come in. They help you test code in isolation, without dragging in the whole ecosystem.
Let’s decode the trio:
🧪 Stub
A stub gives back hardcoded data — no logic, no flair.
final class StubUserDefaults: UserDefaults {
override func bool(forKey defaultName: String) -> Bool {
return true // Always returns true, no matter the input
}
}🎭 Mock
A mock doesn’t just return values — it also records what happens.
final class MockLogger: Logger {
var didLogError = false
func log(error: Error) {
didLogError = true
}
}Useful for verifying interactions like:
“Did this method actually call the logger when the network failed?”
🧱 Fake
A fake has actual working logic — just simplified. Think of it like a test double with a pulse.
final class FakeDatabase: Database {
private var data: [String] = []
func save(_ item: String) {
data.append(item)
}
func fetchAll() -> [String] {
return data
}
}💡 Real Example Use Case
Imagine testing a UserManager class that:
- Validates credentials
- Logs the user in
- Logs events for analytics
Without mocks/stubs, your test would be a nightmare stew of real API calls and analytics SDKs.
With mocks? You can test just the login logic in peace ✌️
🔄 Want your code to allow this kind of testing magic?
_Here’s a guide to Dependency Injection in Swift_ that makes writing tests 10x easier.
**Dependency Injection in Swift: A Beginner-to-Advanced Guide** _🧪 Feeling tangled in singletons and tightly-coupled code? Learn how Dependency Injection in Swift can help you write…_medium.comhttps://medium.com/swift-pal/dependency-injection-in-swift-a-beginner-to-advanced-guide-b85378c6f8d2
⚠️ Common Unit Testing Mistakes and How to Avoid Them
Even with the best intentions, it’s easy to write tests that end up doing more harm than good. Or worse — tests that look fine but silently lie to you 🫠. Let’s save you from that fate.
Here are some common mistakes (and how to dodge them like Neo in The Matrix 🕶️):
❌ Testing Implementation, Not Behavior
Bad:
XCTAssertEqual(viewModel.internalState, "loading")Good:
viewModel.loadData()
XCTAssertEqual(viewModel.items.count, 3)Why it’s bad: You’re locking your test to how something works instead of what it does. If you refactor your internal state later, the test breaks for no real reason.
❌ Over-Mocking Everything
Mocking is great… until your test has 7 mocks and you have no idea what’s being tested anymore.
If your test setup looks like:
let mockA = MockA()
let mockB = MockB()
let mockC = MockC()
// ...You’re probably trying to test too much in one go. Split your logic — and your tests — into smaller chunks.
❌ No Assertions (a.k.a. “Tests” That Don’t Test)
We’ve all been there:
func testSomething() {
let result = doSomething()
print(result)
}This test is just a glorified playground. Add an XCTAssert or it’s not pulling its weight.
❌ Ignoring Async or Timing Issues
Async code can be sneaky. A test might pass today and fail tomorrow — because it didn’t actually wait for the result.
Use XCTestExpectation or even better, Swift’s native async/await with:
func testSomethingAsync() async throws {
let result = try await viewModel.fetchData()
XCTAssertEqual(result.count, 5)
}Reliable tests = trustworthy tests ✅
❌ Skipping Testable Architecture
If your code is tightly coupled and hard to test, it’s not just a testing problem — it’s an architecture problem.
🧱 Not sure which architecture plays nice with tests?
_This breakdown of MVC vs MVVM vs VIPER_ can help you choose what’s best for your team and sanity.
**MVC vs MVVM vs VIPER in iOS: Which Architecture Should You Choose in 2025?** _In this no-fluff breakdown, we’ll compare the good, the bad, and the “wait… why is this so complex?” of each…_medium.comhttps://medium.com/swift-pal/mvc-vs-mvvm-vs-viper-in-ios-which-architecture-should-you-choose-in-2025-38386312e0c1
⏱️ Bonus: Testing Async Code in Swift (The Easy Way)
Testing async code used to feel like a mix of hope, magic, and XCTestExpectation voodoo. But with Swift’s modern concurrency, writing async tests is finally clean and readable. 🙌
Let’s compare both styles quickly — so you know which one to reach for.
🧙♂️ Old-School Way:
XCTestExpectation
If you’re dealing with code that uses completion handlers, this still works:
func testLoginSuccess_withCompletion() {
let expectation = XCTestExpectation(description: "Login succeeds")
viewModel.login(email: "user@test.com", password: "password") { success in
XCTAssertTrue(success)
expectation.fulfill()
}
wait(for: [expectation], timeout: 2.0)
}Useful, but… a bit clunky. Let’s modernize it.
⚡ The Swift Concurrency Way: async/await in Tests
If your methods are marked async, your test can be too:
func testFetchUser_returnsCorrectUser() async throws {
let user = try await userService.fetchUser(id: "123")
XCTAssertEqual(user.name, "Karan")
}Boom — no expectations, no wait, no nonsense. Just beautiful async test flow.
You can also test throwing functions like this:
func testFetchUser_withInvalidID_throwsError() async {
do {
_ = try await userService.fetchUser(id: "invalid")
XCTFail("Expected error, but got success")
} catch {
XCTAssertTrue(error is NetworkError)
}
}Yes, your tests can finally look like real Swift code. 😌
✅ Pro Tip: Use @MainActor if Testing UI-Related Code
If you’re testing something tied to the UI or @Published properties:
@MainActor
func testViewModelUpdate() async {
let vm = YourViewModel()
await vm.loadData()
XCTAssertEqual(vm.state, .loaded)
}It helps avoid nasty threading issues and matches your actual runtime behavior.
Want to understand Swift’s concurrency model deeper (and avoid async gotchas)?
Check out _Mastering async/await in Swift (Part 1)_
**Mastering async/await in Swift: A Beginner’s Guide to Modern Concurrency (Part 1)** _Confused by Swift’s async/await? Learn the basics of async/await, how it improves your Swift code, and how to get…_medium.comhttps://medium.com/swift-pal/mastering-async-await-in-swift-a-beginners-guide-to-modern-concurrency-part-1-88cdb659ac3b
🧘 Closing Thoughts: Make Testing a Habit, Not a Hassle
Unit testing isn’t just for massive teams or “enterprise” apps with 72 developers and a Jira board that looks like a crime scene 🧩. It’s for you, the indie dev, the solo hacker, the side-project builder — anyone who wants their code to just work.
Here’s what we hope you take away:
- Start small. Even one well-written test is better than none.
- Focus on testing behavior, not just lines of code.
- Use dependency injection, mocks, and clean architecture to make your code testable from the start.
- Embrace async/await — it makes async testing not just bearable, but actually clean.
- And most importantly: don’t aim for 100% coverage. Aim for meaningful coverage.
You don’t need to test everything. Just test what breaks your heart when it breaks 😅
🔗 Want to Go Deeper?
Here are some great next reads to level up your skills:
- 🧱 SOLID Principles in Swift
- 🧪 Dependency Injection in Swift
- 🧰 Modular Architecture for Scalable iOS Apps
- 🧠 MVC vs MVVM vs VIPER
- 🚀 Mastering Async/Await in Swift
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read