Mocks, Fakes, and More: Understanding Test Doubles in Kotlin
When writing tests in Kotlin, especially for Android development, we often need to replace real dependencies with test doubles. However, not all test doubles are the same—terms like mocks, fakes, stubs, spies, and dummies often come up. In this post, we’ll break down their differences with Kotlin examples using only plain Kotlin (no third-party libraries).
1. Understanding Test Doubles
Test doubles are objects that stand in for real dependencies in tests. They help us isolate the system under test (SUT) and make tests more reliable. Here are the main types:
Dummy – A placeholder object that is never actually used.
Stub – Provides predefined responses but doesn’t contain logic.
Fake – A lightweight implementation with in-memory logic.
Mock – A test double that verifies interactions.
Spy – Wraps a real object while allowing selective behavior modification.
2. Dummy Objects
A dummy is an object that exists only to satisfy a method signature but is never actually used.
interfaceEmailSender{funsendEmail(email:String,message:String)}classUserService(privatevalemailSender:EmailSender){funregisterUser(user:User){// User is registered, but we don't actually send an email
}}dataclassUser(valname:String,valemail:String)// Test
funtestRegisterUser(){valdummyEmailSender=object: EmailSender{overridefunsendEmail(email:String,message:String){// This will never be called in the test
}}valuserService=UserService(dummyEmailSender)userService.registerUser(User("John Doe","john@example.com"))}
3. Stubs
A stub returns predefined responses to method calls but doesn’t track interactions.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interfaceUserRepository{fungetUser(id:Int):User?}classStubUserRepository:UserRepository{overridefungetUser(id:Int):User?{returnif(id==1)User("John Doe","john@example.com")elsenull}}// Test
funtestGetUser(){valstubRepo=StubUserRepository()valuser=stubRepo.getUser(1)assert(user?.name=="John Doe")}
4. Fakes
A fake is a simplified but functional version of a real class, often using in-memory storage.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
classFakeUserRepository:UserRepository{privatevalusers=mutableMapOf<Int,User>()overridefungetUser(id:Int):User?=users[id]funaddUser(id:Int,user:User){users[id]=user}}// Test
funtestFakeUserRepository(){valfakeRepo=FakeUserRepository()fakeRepo.addUser(1,User("Jane Doe","jane@example.com"))assert(fakeRepo.getUser(1)?.name=="Jane Doe")}
5. Mocks
A mock is a test double that verifies interactions. Without a mocking framework, we must manually track calls.
classMockEmailSender:EmailSender{varwasSendEmailCalled=falsevarsentTo:String?=nullvarsentMessage:String?=nulloverridefunsendEmail(email:String,message:String){wasSendEmailCalled=truesentTo=emailsentMessage=message}}// Test
funtestSendWelcomeEmail(){valmockEmailSender=MockEmailSender()valservice=NotificationService(mockEmailSender)service.sendWelcomeEmail(User("test@example.com","test@example.com"))assert(mockEmailSender.wasSendEmailCalled)assert(mockEmailSender.sentTo=="test@example.com")assert(mockEmailSender.sentMessage=="Welcome!")}classNotificationService(privatevalemailSender:EmailSender){funsendWelcomeEmail(user:User){emailSender.sendEmail(user.email,"Welcome!")}}
6. Spies
A spy wraps a real object while allowing selective behavior modification. Without a library, we must extend the real class and override specific behavior.
Example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
openclassMathService{openfunadd(a:Int,b:Int)=a+b}classSpyMathService:MathService(){varwasAddCalled=falsevarlastA:Int?=nullvarlastB:Int?=nulloverridefunadd(a:Int,b:Int):Int{wasAddCalled=truelastA=alastB=breturnsuper.add(a,b)// Calls real implementation
}}
7. Using MockK for Mocks and Spies
While manually creating mocks and spies is possible, using a library like MockK simplifies the process.
MockK provides powerful features like automatic spies, relaxed mocks, and argument capturing, making testing easier and more maintainable.
Conclusion
Understanding test doubles helps you write better tests by isolating dependencies. Use:
✅ Dummies when an argument is required but unused. ✅ Stubs for returning predefined values. ✅ Fakes for lightweight implementations. ✅ Mocks when verifying interactions. ✅ Spies when you need partial mocking. ✅ MockK for easier and more powerful mocking.
By choosing the right type, you can make your tests more reliable and maintainable.