Have you ever spent all night stuck because a Singleton made testing impossible?
The logic is fine, but adding tests keeps hitting the real DB and calling the real API.
Let me give you the conclusion first.
Because a Singleton is accessed directly in the code, you cannot swap it for a fake object during testing.
Dependency injection, by contrast, supplies required objects from outside, so you can replace them freely in tests.
Today, we will walk through the difference between the two with real code.
Why Do Singletons Block Testing?
A Singleton is an object that exists exactly once across the entire program.
It is convenient: you can retrieve it anywhere with just Database.shared.
That is where the problem begins.
When an object is retrieved directly inside a function, that function becomes tightly coupled to the real database.
There is no way to tell it, “Use a fake DB instead of the real one” during testing.
// Access the Singleton directly inside the function
func saveOrder(_ order: Order) {
// Become tightly coupled to the real DB
let db = Database.shared
db.insert(order) // No way to block this in tests
}
To test this code, the real DB must be running.
If the DB is down, the test fails too.
The logic is not wrong, but the infrastructure makes the test turn red.
How Does Dependency Injection Work?
Dependency injection is less elaborate than it sounds.
Instead of retrieving an object inside a function, create it outside and pass it in.
You literally “inject” what is needed.
// DBReceive it from outside as a parameter
func saveOrder(_ order: Order, db: Database) {
db.insert(order) // The caller decides which DBit is
}
That is a one-line difference, right?
We only changed what was retrieved directly with Database.shared to be received as a parameter.
Yet this small change completely transforms testing.
Use the real DB in production code and a fake DB in tests.
Receiving it through the initializer is also common.
class OrderService {
private let db: Database
// Receive and store the object from outside when creating it
init(db: Database) { self.db = db }
}
Once received, it can be used anywhere in the class as self.db, which is much cleaner.
Swapping in a Fake Object for Tests
Now we get to the really interesting part.
Since the dependency now comes from outside, use a fake DB instead of the real one in tests (usually called a mock).
@Test func When the order is_saved, it is_DBpassed to_() {
let fake = FakeDatabase() // Fake, not real
saveOrder(Order(), db: fake)
#expect(fake.savedCount == 1) // Check only that save was called
}
There is no need to start the real DB.
No network or external server is needed either. It finishes instantly in memory.
As a result, tests become faster and, above all, stable.
You can verify only the logic, regardless of external conditions.
Singleton vs. Dependency Injection: At a Glance
Here is their difference summarized in a table.
| Category | Direct Singleton use | Dependency injection |
|---|---|---|
| Where the object comes from | Retrieved directly inside the function | Passed in from outside |
| Replacing the fake object | Practically impossible | Freely possible |
| Test speed | Slow (real resources required) | Fast (processed in memory) |
| Test stability | Affected by external conditions | Only the logic can be verified |
| Coupling | High | Low |
Singletons themselves are not always bad.
They are convenient when exactly one instance is truly needed, such as for configuration values.
The problem is the habit of “retrieving them directly anywhere.” That ties the hands of your tests.
Frequently Asked Questions (Q&A)
Q. Do I absolutely need a DI library such as Swinject to use dependency injection?
No. Passing dependencies through parameters or an initializer, as in today’s code, is already dependency injection. A library only manages this automatically.
Q. Does it get messy when there are too many parameters?
Yes. That is why dependencies are usually received together through initializer injection or grouped by relationship. If the parameters keep increasing, it may also signal that the class is doing too much.
If your tests keep getting held back by real resources, change just one place in the direction discussed today.
Change code that retrieves objects directly so it receives them from outside.
That small habit makes testing enjoyable. Happy coding!

