Welcome to the first lesson of our course on managing test doubles. In this lesson, we will explore the concept of dependencies in software testing and introduce you to the use of test doubles, starting with "dummies," the simplest form of test double.
Dependencies are elements or services that your software relies on to function, like databases, logging systems, or external APIs. However, when testing, these dependencies can introduce variability, making it difficult to test your code's logic reliably. Test doubles allow you to replace these real dependencies with simpler objects that mimic their behavior. This ensures tests focus solely on your code's logic without interference from external systems. For instance, by isolating an email service’s logging component using a test double, you can test email-related functionality without generating actual log entries.
During this course, we'll discuss five kinds of test doubles:
- Dummies: Simple placeholders used to fulfill parameter requirements. They have no logic or behavior beyond satisfying an interface or method signature.
- Stubs: Provide predefined responses to specific calls during testing, allowing you to control the behavior of certain dependencies without implementing full functionality.
- Spies: Track information about interactions with dependencies, such as method calls and arguments, enabling you to verify behaviors indirectly.
- Mocks: More sophisticated test doubles that are both stubs and spies. They allow you to set expectations and verify that certain interactions occur during testing.
- Fakes: Simpler implementations of complex behavior that are useful for testing, typically with some working logic, often used to simulate a real system or component.
In this lesson, you'll learn how dummies provide a straightforward way to address dependencies by serving as simple placeholders without any logic. By the end, you'll have a foundational understanding of how to utilize dummies in your workflow, paving the way for more complex test doubles in future lessons.
Before diving into the dummy implementations, let's look at the interfaces that define the contracts for the dependencies used by the EmailService
. These interfaces ensure the service can work with different implementations, promoting flexibility and testability.
Kotlin1interface ILogger { 2 fun log(message: String) 3}
The ILogger
interface defines the contract for logging messages, which EmailService
relies on to log email-related actions.
Kotlin1interface IEmailSender { 2 fun send(to: String, subject: String, body: String) 3}
The IEmailSender
interface specifies the methods required for sending emails, which EmailService
uses to delegate email-sending functionality.
Let's see dummies in action by setting up tests for an EmailService
application using JUnit in Kotlin. Here's how you can create an EmailServiceTest.kt
file with dummies:
Kotlin1import org.junit.jupiter.api.BeforeEach 2import org.junit.jupiter.api.Test 3import kotlin.test.assertTrue 4 5class EmailServiceTest { 6 7 private lateinit var emailService: EmailService 8 9 @BeforeEach 10 fun setUp() { 11 emailService = EmailService(DummyLogger(), DummyEmailSender()) 12 } 13 14 @Test 15 fun shouldAcceptValidEmailParameters() { 16 val result = emailService.sendEmail( 17 "test@example.com", 18 "Hello", 19 "This is a test" 20 ) 21 assertTrue(result) 22 } 23 24 // Additional tests for other cases... 25}
In this example:
DummyLogger
andDummyEmailSender
act as stand-ins for real implementations. They don't have any behavior; they just satisfy the interface requirements ofEmailService
.- By using dummies, you reduce complexity and ensure that the tests are focusing solely on the logic within
EmailService
.
This method provides an introduction to isolating dependencies with minimal effort, setting the stage for learning about more advanced test doubles like stubs, mocks, and fakes.
Below are the dummy classes used in the EmailServiceTest
. These simple implementations satisfy the required interfaces without performing any real actions.
Kotlin1class DummyLogger : ILogger { 2 override fun log(message: String) { 3 // Do nothing 4 } 5}
The DummyLogger
implements the ILogger
interface but leaves the log
method empty, serving only as a placeholder.
Kotlin1class DummyEmailSender : IEmailSender { 2 override fun send(to: String, subject: String, body: String) { 3 // Do nothing 4 } 5}
Similarly, DummyEmailSender
implements IEmailSender
but provides no actual functionality in the send
method. It only fulfills the dependency requirements of EmailService
.
To fully understand how dummies integrate into your testing workflow, let's look at the implementation of the EmailService
that the tests are targeting. Here's how you can set up the EmailService
:
Kotlin1class EmailService(private val logger: ILogger, private val emailSender: IEmailSender) { 2 3 fun sendEmail(to: String?, subject: String?, body: String?): Boolean { 4 if (to == null || !to.contains("@")) { 5 return false 6 } 7 if (subject == null || subject.isEmpty()) { 8 return false 9 } 10 if (body == null || body.isEmpty() || body.length > 1000) { 11 return false 12 } 13 logger.log("Sending email to $to") 14 emailSender.send(to, subject, body) 15 return true 16 } 17}
This implementation outlines a basic EmailService
class, using a logger and an email sender as dependencies. The method sendEmail
performs basic validation checks on the email parameters and, if they're valid, logs and sends the email using the dependencies provided. By using dummies to stand in for ILogger
and IEmailSender
, you can isolate this email-sending logic from the complexities of actual logging and email-sending functionalities, ensuring a focused test environment.
In this lesson, you were introduced to the concept of dependencies in testing and how test doubles, specifically dummies, can help isolate these dependencies. Dummies serve as simple placeholders without behavior, allowing you to concentrate on testing the core logic of your application without external complexities.
By using dummies, you can simplify test setups and focus more effectively on the behavior of the code under test. This foundational understanding sets the stage for exploring more advanced test doubles in future lessons.
