Software Design

An Introduction to Spies in RSpec

An Introduction to Spies in RSpec

by Sam Phippen

Introduction to Spies in RSpec

In this talk, Sam Phippen introduces the concept of spies in RSpec, a testing framework for Ruby. The session is designed for beginners who wish to enhance their understanding of RSpec and improve their testing practices.

Key Points Discussed:

  • Importance of Testing: Sam emphasizes the importance of integrating testing into everyday software development, particularly within the Ruby community, highlighting how this practice helps developers confidently make changes to complex applications.
  • Mental Models and Tests: Tests serve as a way to serialize knowledge about software. As applications grow, tests help maintain an understanding of their functionality, making it easier to implement features and identify bugs.
  • Types of Tests: Sam differentiates between two primary types of tests:
    • Integrated Tests: These tests examine the entire application, including databases and external systems. They provide a comprehensive overview of system functionality.
    • Isolated Tests: In contrast, isolated tests focus on single components by faking dependencies, helping to isolate functionality and exert design pressure on the code.
  • Role of Stubs, Mocks, and Spies: Sam explains the use of stubs and mocks in testing:
    • Stubs: These allow for specific method calls to return predetermined responses without verifying interactions.
    • Mocks: These not only replace method implementations but also confirm whether methods are called during tests.
    • Spies: Unlike stubs and mocks, spies do not change method implementations. Instead, they are injected into the test to observe interactions, allowing the tester to check if and how methods are called.
  • Practical Demonstration: The talk includes a live coding session demonstrating how to use spies within RSpec to monitor interactions between objects. Sam shows how to create a spy, set expectations for method calls and arguments, and verify the number of method invocations.
  • Example Application: A concrete example is given of an API wrapper for an HTTP service where Sam demonstrates testing interactions with the HTTP client, utilizing mocks and spies to validate that requests are made correctly.
  • Common Concerns: Sam addresses concerns about transitioning to RSpec 3, recommending attendees check the comprehensive online upgrade guide to ease the process.

Conclusions and Takeaways:

  • Writing tests significantly contributes to better software design and helps developers manage complexity.
  • Utilizing spies in RSpec allows developers to write efficient tests that focus on interactions without altering their original codebase.
  • Effective test practices enhance the maintainability and reliability of applications.

Overall, Sam aims to foster a better understanding of RSpec's spying mechanism and its applications within Ruby software development, encouraging developers to improve their testing practices for greater software quality.

00:00:18.160 Hi everyone, thanks for coming. This is my talk entitled 'An Introduction to Spies in RSpec.' Let's get started.
00:00:26.000 I’m Sam Phippen. You can find me on Twitter and GitHub, where my handle is SamFippen. If you check my GitHub profile, you'll notice that I spend most of my time working as a member of the RSpec core team. That's why I'm here giving this talk today.
00:00:38.800 It’s really important to me that RSpec is represented at community events like this, and that we provide introductions to beginners to help enable them to utilize the testing framework more effectively. I hope everyone leaves today having learned something valuable about RSpec.
00:01:04.479 Currently, I work for a company called Fun and Plausible Solutions, which is a consulting agency for data science problems. We tend to work with companies that excel at building exceptional web and mobile applications but may not have expertise in areas like machine learning, recommender systems, or A/B testing.
00:01:17.200 If you’re trying to implement those concepts in your own work and are facing challenges, please feel free to chat with me afterward. I love discussing these topics.
00:01:52.479 I want to preface this talk by stating that my goal isn't to present a grand theory on software engineering or lay out what we should be doing ten years from now. Instead, I want to share some interesting insights about testing and how RSpec works. My hope is that everyone in attendance learns something today.
00:02:19.519 If you find something confusing or if you'd like me to elaborate on any point, please feel free to interrupt me and ask questions. More than likely, if you have a question, someone else in the room will too.
00:02:41.440 This talk will focus on testing software in Ruby and the tools we utilize for it. One remarkable aspect I've observed when working with people using various programming languages, such as Python and Java, is that the Ruby community has wholeheartedly embraced testing.
00:03:10.480 They have integrated testing into their everyday practices more effectively than many other programming communities. It's truly astounding how much testing we do, even at a beginner level. Even if the tests aren't perfect or have issues, they still enable me to confidently make changes to an application I've never encountered before without worrying about breaking anything.
00:03:38.320 I want to share my thoughts on why writing tests—specifically building automated testing for software—is invaluable. One perspective to consider is how we form mental models of the software we're creating. Initially, when you begin working on an application, you can hold nearly everything it does in your mind. This makes it easy to adapt and confidently implement changes. However, as time progresses and our product managers and users present us with feature requests or bug reports, our software becomes increasingly complex.
00:04:48.080 Consequently, it becomes harder to retain an understanding of everything your software is doing. This is where tests come into play. Tests allow us to serialize knowledge about our applications into an executable form, which is often a struggle for beginners. They may comprehend the concept of behavior verification but see it as a short-term goal. However, I argue that having a robust test suite filled with knowledge is fundamental for any application.
00:05:27.040 As that knowledge grows, it allows your team to expand and continue evolving your software. Moreover, writing tests alongside developing software helps identify bugs in new features as they are being built. This ensures that the feature is more likely to work when integrated with others, making it less likely for those bugs to recur.
00:06:07.840 When a bug does make it through to our users, writing a test that reveals that bug and fails when the bug is present can confirm that after fixing the bug, it won't reappear. This is an incredibly useful property in software development.
00:06:51.440 Moreover, we can write tests that can improve the design of the software we're creating. Certain types of tests direct our focus to specific pieces of code, naturally enhancing the design and software architecture. However, to delve into this topic in depth, we first need to discuss the types of tests available.
00:07:15.199 I want to begin with the type of test that I believe most beginners write, which is an integrated test. An integrated test assesses your entire application by packaging everything together, including your database and external systems such as email servers or Amazon services. It tests the entire system as a whole, interacting with it as a user would without faking any part of the environment it operates in.
00:08:04.639 This kind of integrated testing is invaluable for certain behaviors we expect within our systems. Typically, when an integrated test fails, we can definitively conclude that our application is broken. Conversely, when it passes, it signals that the application might be functioning properly, making those keywords crucial.
00:09:00.880 On the other end of the spectrum, we have an isolated test. An isolated test focuses on a single component of your application, isolating it from all dependencies and collaborators. This forces you to concentrate on the specific implementation of that piece of functionality.
00:09:47.600 To achieve isolated testing, you have to fake parts of the world that test touches, which means when writing an isolated test, you're essentially obscuring as much of your application as you can from that particular piece of code to test it independently. Isolated tests, due to their focus, exert design pressure on your code. If one finds it hard to write an isolated test, it usually indicates a problem with the design of your system.
00:10:49.120 This concept fundamentally relates to coupling; if your component is loosely coupled to other parts of your application, it will be easier to isolate for testing. If it is tightly coupled to several parts of your system, isolated testing becomes challenging.
00:11:18.080 It's essential to note that a variety of tests exist between integrated and isolated tests. For example, if you were building an application that utilizes a service-oriented architecture, you could extract a single service and test that in isolation while mocking the other services it interacts with. This demonstrates that there are different levels of isolation that can be applied in tests.
00:12:01.760 Now, let's explore the idea of faking different components of our system. To illustrate this point, I’d like to use an analogy from Justin Searles, who will be giving the closing keynote at this conference. Imagine we are developing a GPS system for a Boeing 747. We might conduct integrated tests, putting it in an aircraft and flying it to see if it works, but that would be prohibitively expensive and dangerous.
00:12:38.640 Instead, we can isolate the GPS unit, thoroughly testing it to ensure it works correctly before installing it in an airplane. This approach is not only more cost-effective but minimizes risk significantly. Similarly, testing software applications often involves interacting with databases or services, which can incur high costs and dependencies.
00:13:05.280 This brings us back to faking parts of our application. In RSpec, as well as many other testing frameworks, we use the concept of a stub. The role of a stub is to take a component that we wish to isolate and fake a response for one of its method calls. With a stub, you choose an arbitrary object that your isolated component collaborates with and replace one of its methods with a stub implementation.
00:14:06.119 This simple implementation allows you to focus entirely on the object you care about while ignoring the collaborator's internals. However, stubs do not allow you to verify whether the interactions between objects occur, which can be an essential aspect of testing.
00:14:53.440 To confirm that a dependent object is being interacted with as expected, we use mocks. Mocks are similar to stubs but take it a step further by not only replacing the implementation of a method but also checking whether that method is called during the test. If the expected call is not made, the test fails, highlighting the interaction.
00:15:54.720 Now let's discuss spies, which is the main subject of this talk. Spies differ from both stubs and mocks because they don't replace the implementation of any individual method. Instead, they are objects in their own right.
00:16:10.560 When you utilize a spy in your tests, you're creating a new object and injecting that object into your test to isolate your collaborators. If none of this is clear yet, don't worry. I’ll perform some live coding now to clarify things.
00:16:43.040 As we start, let me first check that everyone can see the code on my display. If not, please let me know. Let's dive into a simple RSpec test template. Here, we load a file called spec_helper via require, which establishes some default configurations for our RSpec test suite. Then we utilize the describe method to set up a group of tests.
00:17:31.200 Within this context, I'm going to demonstrate how RSpec spies operate. The way to access a spy object in RSpec is by invoking the spy method within the body of your test. Using the it method, I set up a new test, and in it, I create a pointer to a spy object named 'my_spy'. To set expectations on method calls, we use normal RSpec syntax.
00:18:10.240 For instance, we can assert that we expect 'my_spy' to receive the method 'foo'. When we run the test, it will fail since we haven't called 'foo' on 'my_spy'. To fix this, we only need to invoke 'my_spy.foo', and upon rerunning the test, it should pass.
00:18:52.800 This is how RSpec spies work: when the 'foo' method is invoked on the spy object, that call gets recorded. You can also match arguments during tests: by adding a call to the have_received matcher, we can verify that 'foo' was called with specific arguments.
00:19:44.080 For example, if I want to ensure the method 'foo' is called with arguments One, Two, Three, I can set that expectation. If it’s not called with those specific arguments, the test will fail.
00:20:15.440 Additionally, we can validate the number of times a method call occurs. To enforce that 'foo' is called four times, we can use the syntax expect(my_spy).to have_received(:foo).exactly(4).times. This method chaining allows for clearer readability.
00:20:39.120 Finally, let's move on to examining a test for an actual piece of Ruby code. I've created an object called counter_client, designed to provide an API wrapper around a simple HTTP service that I developed. This customization enables easy counting based on string keys.
00:21:32.720 With our set of integrated tests for the counter_client, we verify its behavior effectively: If no increment occurs for a key, calling the get method returns zero. Incrementing it once results in one being returned. We can also return random values for several increments, ensuring that everything functions as intended.
00:23:09.680 However, our tests don't actually verify that HTTP requests are made. Thus, we will demonstrate how to utilize mocks and spies to confirm these interactions. Initially, I'll add a test for the get method and set up a mock to check that a specific request is made to our HTTP client.
00:24:35.200 In the code, I'll apply the RSpec idiom, mentioning the get method within the describe block for clarity. By employing RSpec's expect method, I can assert that calling counter_client.get with a specific key correctly triggers the HTTP request.
00:25:31.600 After commenting out the mock, running the tests shows that failures occur for all tests except the one using mock expectations, confirming that the interaction is effectively detected.
00:26:43.559 While we've successfully verified the collaboration with the http client, we should recognize that this test has issues. Its operational order deviates from that in other tests, breaking the Arrange-Act-Assert pattern which can complicate readability.
00:27:54.560 To remedy this, I aim to adjust the design of both the counter client and the test. By moving the HTTP client instantiation outside of the individual tests, we can encode the client more cleanly, improving our code’s design.
00:29:01.280 Once that’s achieved, we can implement a double to stand in for the HTTP client, allow for various test arrangements, and eventually revert to using a spy to continuously adhere to the Arrange-Act-Assert pattern.
00:30:49.840 This concludes the primary coding demonstration. Now, I’ll switch back to my slides for a final discussion. As you may have detected from my accent, I’m not local to these parts. I’m British and traveled a considerable distance for this event.
00:31:33.280 Now, I have a mini-rant regarding the standard of tea available here. I adore tea as a soothing beverage, but finding a good cup is a challenge in this country. Even professional tea establishments have been disappointing in my experience.
00:32:11.200 It's important to prepare water correctly for hot drinks: coffee brews best at a temperature of 92 to 95 degrees Celsius, but tea needs boiling water. So if you take away one lesson from this talk, let it be to boil your water properly when making tea.
00:32:59.840 Now that I’m done, are there any questions? One question I've heard often is about RSpec 3, which was recently released and includes breaking changes. I know this can be daunting; your test suites are crucial to your applications.
00:33:52.799 The good news is there’s a comprehensive upgrade guide available online that many people overlook, as it’s not overly intrusive. I recommend checking it out for an easier experience with the update. I’m Sam Phippen on Twitter and GitHub, and my email is [email protected] if you’d like to discuss anything further.
00:35:00.320 Thank you for listening to my rant today! I'm happy to engage with all of you.