Welcome to our third lesson on Test-Driven Development (TDD) with Test Doubles. In this lesson, we focus on Spies, an essential type of test double used to observe interactions with your code's dependencies during testing. By now, you've already been introduced to Dummies and Stubs in previous lessons, which allow you to manage dependencies via test doubles effectively.
Our goal here is to seamlessly integrate Spies into the TDD Red-Green-Refactor cycle: writing tests (Red), creating minimal implementations (Green), and refactoring for better quality without altering behavior. Let's dive into understanding and using Spies within this framework using Swift's XCTest framework and custom Spy implementations.
Spies are a powerful tool that can be implemented in Swift to observe and record how functions in your application are used without modifying their behavior. In TDD, Spies help verify that functions are called when and how you expect them to be, which is a vital part of writing reliable tests.
Spies can check:
- If a function was called
- How many times it was called
- With what arguments it was called
They align perfectly with the Red-Green-Refactor cycle:
- Red: Write a failing test to ensure your code's behavior is verified.
- Green: Implement only enough code for the tests to pass.
- Refactor: Clean up the tests and the implementation for better software design.
What distinguishes Spies from other test doubles is their focus on behavior verification rather than just input/output. This means that instead of asserting what the system returns, you assert how the system interacts with its dependencies. This is especially helpful when your application doesn’t return a result directly but instead performs a side effect, like sending a notification.
Let's set up our environment using XCTest
to harness this powerful tool.
Let's consider a Notification Service example where we aim to ensure notifications are sent with appropriate priorities. We will begin by implementing a Spy on the send
method of the RealNotificationSender
. This allows you to use an actual dependency within your test and verify how it was called.
Here's an example test file: NotificationServiceTests.swift
.
- We set up our testing environment using
XCTest
. - A Spy is created by subclassing or implementing the
NotificationSender
protocol. - This Spy will help us verify interactions with the
send
method.
Next, we insert failing tests to see our Spies in action. Remember that writing failing tests is our "Red" step in TDD.
Let's add some test cases to observe the behavior of the send
method using our Swift Spy.
- We create a failing test to ensure our notification sends with low priority.
- The Spy captures and observes interactions of the
send
method. - The assertions verify how often it was called and with specific parameters.
Upon running this test, it might fail initially as we have not written the corresponding implementation. This is the Red step of our cycle. Implementing minimal logic in the NotificationService
class will move us towards making the test pass (Green).
After writing a failing test to capture how our send
method should behave, the next step is to implement the minimal functionality required to make the test pass. This involves modifying the NotificationService
class to utilize the send
method correctly based on the specified logic.
Here's an example of updating the NotificationService
to fulfill the test requirements:
- The
send
method is then invoked with theuserId
,message
, and calculatedpriority
("low"
for now). - Since there is only one test, this is the minimal amount of code necessary to get the test to pass.
With this implementation, re-running the test should now pass, taking us from the Red to the Green phase of TDD.
In the example above, you see how we can assert that a function being spied on is called. But what if you have multiple calls to the spied function that you want to verify? You can extend the Spy to record all calls and their arguments:
- We assert that the
send
method has been called three times in our test scenario. - The Spy records each call, allowing us to verify the sequence and arguments of each call.
- This ensures that not only the number of calls but the sequential invocation and arguments for each call are exactly as expected.
In this lesson, we've explored the importance of Spies as test doubles in the TDD process using Swift and XCTest. Here's what we've achieved:
- Red: Set up tests simulating real-world scenarios, initially leading them to fail.
- Green: We strategized to write the least amount of functionality to fulfill all test criteria.
- Refactor: Ensured our code and tests remain clean, scalable, and comprehensible.
As we transition into practice exercises, focus on implementing Spies in different scenarios to solidify these concepts. The exercises will guide you in writing, verifying, and refactoring tests with Spies, empowering you to integrate this knowledge into complex projects.
Happy testing, and enjoy the lessons you've built through TDD in Swift with XCTest!
