Test Doubles Part 2 : Understanding Dummies
Dummy objects are passed around but never actually used. Usually they are just used to fill parameter lists. “Martin Fowler”
When writing tests, especially unit tests, it is important to focus on the behavior being tested. Sometimes, the system under test (SUT) relies on components or objects that are not relevant to the test scenario. To handle these, we use dummy objects and dummy values as placeholders to set up the system without affecting the test outcome. Let’s look at these concepts in detail with examples in Swift.
What Are Dummy Values?
Dummy values are simple placeholders used to replace fields or inputs that are not relevant to the test. Unlike dummy objects, these are typically used for primitive types like String
, Int
, or Bool
.
Example: Using Dummy Values
Suppose we have a method that calculates a discount for a user based on their membership level. If we only care about testing the discount logic, we can use dummy values for unrelated fields.
struct User {
let name: String
let age: Int
let city: String
let membershipLevel: Int
}
func calculateDiscount(for user: User) -> Double {
switch user.membershipLevel {
case 1: return 0.05
case 2: return 0.10
case 3: return 0.20
default: return 0.0
}
}
Test with Dummy Values
In this test, only the membershipLevel
field is relevant:
func testCalculateDiscount_ForMembershipLevel3_ShouldReturn20Percent() {
// Arrange
let user = User(name: "Dummy", age: 0, city: "Dummy", membershipLevel: 3)
// Act
let discount = calculateDiscount(for: user)
// Assert
XCTAssertEqual(discount, 0.20, "Discount for level 3 should be 20%")
}
Here, name
, age
, and city
are dummy values that have no impact on the test.
What Are Dummy Objects?
A dummy object is a stand-in object used to meet a dependency without being actively utilized in the test. The main purpose of a dummy object is to:
- Fulfill the dependencies needed to initialize the system.
- Show clearly that the dependency is irrelevant to the current test.
Example: Using Dummy Object
We have a UserService
that depends on a Logger
. However, the logger isn't used in the validateUser
method, so we can use a dummy object to satisfy this dependency.
Step 1: Define Dependencies
1. Protocol for the Logger
The Logger
is a dependency of UserService
. We'll define it as a protocol:
protocol Logger {
func log(message: String)
}
2. User Model
The User
model represents the user being validated:
struct User {
let name: String
let age: Int
let city: String
}
3. User Validator
A simple utility for validating users:
struct UserValidator {
static func validate(user: User) -> Bool {
return !user.name.isEmpty && user.age > 0 && !user.city.isEmpty
}
}
Step 2: Create the UserService
The UserService
depends on the Logger
but doesn't use it in validateUser
.
class UserManager {
private let logger: Logger
init(logger: Logger) {
self.logger = logger
}
func validateUser(_ user: User) -> Bool {
return UserValidator.validate(user: user)
}
// Other methods that might use the logger
func log(message: String) {
logger.log(message: message)
}
}
Step 3: Create the Dummy Logger
The dummy logger satisfies the dependency but does nothing in its implementation.
class DummyLogger: Logger {
func log(message: String) {
// Do nothing
}
}
Step 4: Write the Test
Here’s how you can write a test for the validateUser
method using the dummy logger.
import XCTest
class UserManagerTests: XCTestCase {
func testValidateUser_WhenAllFieldsAreValid_ShouldReturnTrue() {
// Arrange
let user = User(name: "abdul", age: 29, city: "karachi")
let dummyLogger = DummyLogger()
let userManager = UserManager(logger: dummyLogger)
// Act
let isUserValid = userManager.validateUser(user)
// Assert
XCTAssertTrue(isUserValid, "User is valid when all fields are completed")
}
}
Key Points in the Test
- Dependency Injection: We inject the
DummyLogger
into theUserService
. This allows us to satisfy theLogger
dependency without having to provide a functional implementation. - Focus on Behavior: The test focuses solely on the behavior of the
validateUser
method, without being affected by theLogger
dependency. - Dummy Logger: The
DummyLogger
makes it explicit that the logger is not used in this context, ensuring clarity and intent.
Conclusion
Dummy objects and dummy values are essential tools for effective and focused testing. By using these placeholders, you can keep your tests clean, simple, and meaningful. Whether you’re satisfying a complex dependency with a dummy object or replacing irrelevant fields with dummy values, the key is to ensure your tests are easy to understand and maintain while accurately verifying the behavior of your code.