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.