Fake is a working simplified implementation of a dependency that behaves like a real component but uses in-memory storage or other lightweight mechanisms instead of production infrastructure. Unlike a stub, a fake contains real business logic — sorting, filtering, aggregation — just without external effects. An in-memory database instead of Room or a HashMap instead of SharedPreferences are classic examples. More details in Martin Fowler’s classification of test doubles.
Key Takeaways
Fake is a full but lightweight implementation of an interface suitable for testing. The term was introduced by Gerard Meszaros (2007) in the book “xUnit Test Patterns.” Unlike a stub, which returns hardcoded answers, a fake contains executable code: it can sort a list, filter by condition, count records. The only difference from the production implementation is that a fake works with in-memory data and does not perform real I/O operations.
The main advantage is speed. Tests with a fake execute in milliseconds because there is no disk, network, or database access. An in-memory HashMap works 100–1000 times faster than Room or CoreData. At the same time, a fake tests real business logic: sorting, filtering, aggregation — everything that a stub cannot test because a stub only returns what it was told to. A fake gives confidence that the code correctly processes data, rather than just receiving a predetermined answer.
Fake is preferable to Stub — if the component under test performs multiple operations on data (retrieve, filter, sort, save), a stub would require configuring each call individually. A fake contains the logic inside itself — the test simply calls methods and checks the result. At IT Sectr, we use fakes for all repositories in unit tests: a fake repository with a HashMap covers 90% of scenarios without setting up Mockito or MockK.
Selection criterion — determine what the test verifies: state or interaction. If the test verifies state (the result of work) and uses logic — use a fake. If the test only needs input data without logic — a stub is sufficient. If the test verifies that a method was called — use a mock. Mixing test double types in one test complicates understanding and increases fragility.
| Criterion | Fake | Stub | Mock |
|---|---|---|---|
| Has logic | Yes (simplified) | No | No |
| Speed | High | Maximum | High |
| Behavior verification | Indirect | No | Yes (verify) |
| Maintenance | One class per interface | Configure per test | Configure per test |
| Realism | High (code works) | Low (hardcoded data) | Medium |
| False positive risk | Low | Medium | High (fragile tests) |
Anti-pattern: Fake that isn’t a fake — a common mistake when a developer calls a fake object that is actually a stub or a mock. If your InMemoryUserRepository contains no logic (filtering, sorting) — it’s not a fake, but a stub with in-memory storage. A fake differs from a stub precisely by the presence of executable logic. If a fake repository simply returns what was put into it and does not process data — use a mock or a stub.
Practical recommendation — start with a fake for every repository or service. If a fake exceeds 50 lines — split it into several classes. If a fake is not needed at all (the test only verifies a single scenario with fixed data) — use a stub. If the test verifies that a method was called with specific parameters — use a mock. Don’t optimize the choice in advance: write a fake, and if it turns out to be excessive, replace it with a stub in a specific test.
Fake repository for Room — a typical Android fake example. The production implementation of UserRepository uses Room DAO with SQLite queries. The fake version stores data in a MutableList or HashMap and implements the same methods: getUser(id), saveUser(user), deleteUser(id). The fake contains search, filtering, and sorting logic — the same as the production repository, but without SQL. This allows testing ViewModel and UseCase without setting up a Room database.
class FakeUserRepository : UserRepository {
private val users = mutableListOf<User>()
override suspend fun getUser(id: String): User? {
return users.find { it.id == id }
}
override suspend fun saveUser(user: User) {
val index = users.indexOfFirst { it.id == user.id }
if (index >= 0) users[index] = user
else users.add(user)
}
override suspend fun search(query: String): List<User> {
return users.filter {
it.name.contains(query, ignoreCase = true)
}
}
}
Fake for Retrofit API — instead of MockWebServer (which is a stub, not a fake), you can create an ApiService implementation that returns data from an in-memory collection. The difference: MockWebServer intercepts HTTP and returns JSON, while a fake ApiService works at the Kotlin interface level without serialization. A fake is faster (no JSON parsing) and easier to debug (runs in the same process, typed). Suitable for tests where HTTP semantics (status codes, headers) are not important.
Fake in Swift — built through protocols. The production class implements the protocol with real logic (CoreData, URLSession). The fake structure implements the same protocol with in-memory storage and simplified logic. Swift is a language with value semantics, so fake structures are immutable and safe in multithreaded tests. This gives an advantage over Android analogs: there is no need to synchronize access to in-memory data.
protocol UserRepositoryProtocol {
func getUser(id: String) async -> User?
func saveUser(user: User) async
}
final class FakeUserRepository: UserRepositoryProtocol {
private var storage: [String: User] = [:]
func getUser(id: String) async -> User? {
return storage[id]
}
func saveUser(user: User) async {
storage[user.id] = user
}
}
final class UserViewModelTests: XCTestCase {
func test_save_and_load() async {
let fake = FakeUserRepository()
let vm = UserViewModel(repository: fake)
let user = User(id: "1", name: "Alice")
await vm.saveUser(user)
let loaded = await vm.getUser(id: "1")
XCTAssertEqual(loaded?.name, "Alice")
}
}
Fake for CoreData — in iOS projects, you can create an in-memory NSPersistentContainer by setting description.type = NSInMemoryStoreType. This is a full CoreData stack, but running in memory. Such a fake allows testing NSFetchRequest, predicates, and sorts without creating an SQLite file. Speed: tests on in-memory CoreData run 5–10 times faster than on the disk-based analog. The downside: you need to set up NSManagedObjectModel each time.
FakeURLProtocol — a URLProtocol subclass for intercepting network requests on iOS. It is registered via URLProtocol.registerClass(fakeProtocol). Internally, it contains an in-memory URL -> Data dictionary and returns data without a real request. The difference from a stub: FakeURLProtocol can check the request body, headers, and return different responses depending on the input data. This is a fake because it contains request routing logic.
Fake as a Test Fixture — put fake classes in a shared test module (androidTest/sharedTest or TestSupport). All tests in the project use the same InMemoryUserRepository. This eliminates duplication of mock object setup in every test and guarantees uniform behavior. Changing the fake logic updates all tests simultaneously. At IT Sectr, we store fake classes in sharedTest/java/com/itSectr/fake/ and include them via implementation project(:sharedTest).
Fake with preset data — tests often need a repository that already contains some records. Solution: a factory method fakeWithData(vararg items) or a built-in addDefaultData() method. The factory creates a fake, fills it with typical data, and returns a ready-to-use object. This reduces boilerplate in tests: instead of setting up mock calls, the test simply calls FakeUserRepository.withUsers(alice, bob).
Fake with call counting — sometimes you need to verify not just state but also the number of invocations. A fake can contain counters: saveCallCount, getUserCallCount. The test checks the counter after execution. This is a compromise between a pure fake (state verification) and a mock (interaction verification). Counters do not check arguments or call order — only the count. For argument verification, use a mock.
Fake with Callback — for testing async scenarios, a fake can accept a callback on each call: beforeGetUser, afterSaveUser. This allows simulating delays, errors, or checking intermediate states. This approach is useful for testing UI loading states: the fake pauses for 100 ms, and the test verifies that the screen shows a loader. The callback is absent in production — this is purely test functionality.
Frequently Asked Questions
Fake contains working logic — filters, sorts, counts. Stub only returns predetermined answers without logic. If an object has branches (if/else, when) — it’s a fake. If it only contains return values — it’s a stub. A fake is more expensive to maintain but provides more realistic tests.
When the fake logic does not match the production logic. For example, FakeUserRepository uses case-sensitive search, while the production version uses case-insensitive search. The test passes, but there’s a bug in reality. Solution: test the fake logic separately or use fakes only for interfaces with simple logic (CRUD operations). For complex logic, write integration tests with a real database.
In-memory database is one type of fake. Room.inMemoryDatabaseBuilder() creates in-memory SQLite that behaves like a production database. This is a full-fledged fake. But a fake can also be at the repository level (without SQL) and at the network level (FakeApiService). An in-memory database is a special case of a fake where the logic is as close to real as possible.
Yes, but with caution. Fake for the repository (data), Mock for AnalyticsTracker (event verification). Separation by layers: fake for the data layer, mock for the analytics/logging layer. Don’t make one object both a fake and a mock — this violates the Single Responsibility Principle and confuses the test.
Test the fake with the same tests as the production implementation. If you have a UserRepositoryTest that verifies save, get, delete — run it twice: with FakeUserRepository and with RealUserRepository. This guarantees that the fake replicates the behavior of the production class. If the fake starts behaving differently — the test will fail on both implementations.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also