00:00:17.760
Hi RailsConf! Today, we're going to be talking about testing your services. But before we dive into that, I want to introduce myself. My name is Neal, and I'm originally from Iowa, so I feel right at home here in the Midwest. Now, I live in California, specifically in Los Angeles, and I'm a software developer and independent consultant who works with Ruby on Rails, JavaScript, HTML, and CSS. Basically, if you can name an acronym, I've likely coded with it at some point.
00:00:49.840
Today's talk will cover the what, why, and how of testing your services. This is not a talk about building testable services; I could spend an entire talk on that topic alone. It's also not necessarily focused on test-driven development (TDD). Although I'm a practitioner of TDD, the principles I'll discuss here don't directly correspond to whether you follow TDD or not.
00:01:12.720
So, first, we need to talk about the 'what.' What exactly is a service? I break it down into two main categories: first, we have external services, such as the Twitter API, Facebook's API, or Amazon Web Services; and the second category is internal software-oriented architecture, a buzzword we all know and love. For the purpose of this talk, it essentially means any time you're making an HTTP request to an endpoint in another repository—any network request you make outside of your application.
00:01:56.559
Next, let's discuss the 'why.' We need some justification before we go ahead and test all of our services without question. First, we have to ask ourselves: why are services themselves important? I believe many of these reasons are pretty self-evident. They allow us to build things faster, they facilitate easier scaling, and they are utilized in virtually every application. Personally, I can't think of an app I've built in the past few years that hasn't integrated with multiple services. I'm also observing a growing trend of using more services for every application. Therefore, I would argue that services are critical to modern Rails development.
00:02:31.360
Now, we have to consider the importance of testing those services. Well, first of all, you should be testing everything else, so why should it be different for services? Furthermore, services often compose critical features of your application. For example, consider a Stripe integration: if your billing system goes down, you're going to run into a lot of issues. If you have an API request to S3 and it's down, you won't be able to serve images, which can also lead to problems with these APIs.
00:03:02.800
I know I've encountered unexpected results whenever I've worked with an API. Let me give you an example of an internal API built by consultants in another part of the company. This was the software-oriented architecture we were discussing earlier. They were exposing this API for our Rails app to consume, but we faced many issues along the way. This situation significantly increased the project's length. Sometimes we'd get random null responses when we were supposed to receive objects, random inconsistencies in the output, and strange symbols printed out with different formatting. In general, it was a catastrophe.
00:03:52.799
Thus, it definitely lengthened the time to completion, much of which was due to our failure to test the API thoroughly. We couldn't express the problems we were having to them until the project was put into production. This is one problem that could have been alleviated had we tested first.
00:04:08.000
Next, I want to discuss some problems I've encountered with external APIs. I'm sure many of you have faced similar issues in the past. For instance, do we have any NHL fans in the audience? Yes? The Chicago Blackhawks are doing pretty well in the playoffs so far, but we'll see how it goes. Obviously, they may get crushed by the Kings or Sharks in a few rounds. But let's not start a sports rivalry today!
00:04:41.760
These issues with external APIs have ranged from minor annoyances to major complications. For example, we would receive responses where some returned an ID for a team, and others returned a code. In both cases, they referred to Anaheim, which is a minor annoyance that can be coded around. However, we also encountered an undocumented bug where goals were expected to be part of an array, but if there was only one goal, it would be returned as an object instead. Unfortunately, we discovered this issue in the production environment during a game.
00:05:03.680
Worse still, after we put in all the effort to fix these errors, we realized there was no versioning on the API. Even if we fixed it, we might have to fix it again the following week because of changes made on their end. This unpredictability made working with the API quite frustrating.
00:05:46.400
Let me illustrate my point with another example—a fun side project where I built a Snapchat API client. One of the extreme examples I encountered was with a private Snapchat API that had haphazard or no documentation. I think we've all worked with APIs that have poor documentation, but in this case, we didn't even know what the requests were supposed to be. There were also strange obfuscation techniques used in the app, which made it difficult for developers like me to build anything because they encrypted it on iPhones.
00:06:35.200
Now that we've talked a little about why testing services is important and outlined some of the problems you might encounter, let's talk about how to effectively test these services. First, we need to consider what makes services different from regular code that we test. The main differences are that we have external network requests being made, and we don’t own the code, so we can’t perform unit testing on it. Testing has to be done from an integration test perspective.
00:07:10.480
To facilitate your tests generally, I propose that you turn 'airplane mode' on. This is the best mindset to adopt when thinking about testing. First of all, failure is detrimental in testing, and you should avoid making any network requests. I look at it in two ways: it's airplane mode in the test mode, so those tests are not interacting with the network, and it should also be a test that you can run on an airplane. The idea is that when you're on a long flight or relaxing in the RailsConf lobby, you can run your tests, and they won’t fail due to network issues.
00:07:54.560
This means you should not interact with your external services from your testing environment. However, I will elaborate on a few exceptions while discussing what to avoid. This includes dummy APIs. Some API creators provide their actual API and a fake version that can be hit with requests without modifying your data. Since those are external calls, you shouldn't use those during testing.
00:08:35.440
However, you can set up pre-recorded responses to those endpoints, meaning you can store them within your test suite, which we'll cover in more detail in a bit. For the examples I’ll present, I’ll assume you're using Rails, and for the sake of simplicity, using RSpec.
00:09:21.440
Let’s get started by stubbing out these requests. When stubbing an object, for those unfamiliar, it involves placing a fake object in front of the actual object so that when you hit it, you're using the stub instead of triggering the real object. This saves time with setup processes and other overhead.
00:09:43.760
We're doing something similar when stubbing a request to an endpoint, saving even more time because we’re avoiding the additional network request. There are some libraries that include built-in stubbing, such as Typhoeus, Faraday, and HTTPParty, which are commonly used HTTP libraries built on top of Net::HTTP. However, we can also simplify our approach through a general-purpose stubbing library called WebMock, which many of you might have worked with in the past.
00:10:21.760
Here's a basic spec helper setup. There isn’t much interesting except that you need to include 'disable_net_connect' at the bottom, which I've highlighted. Obviously, with all these examples, you should include the gem in your Gemfile and run `bundle install` before starting.
00:10:50.720
The first time you implement this code, you'll encounter a useful error message that will indicate exactly where in your code you're making network requests. If you’re not already implementing airplane mode tests, plug in 'disable_net_connect', and you'll receive an error detailing where those requests are happening. This is handy because it will provide the request details you’re making at the bottom, allowing you to copy and paste that into your test to stub it automatically. Just ensure you also collect the body and headers if needed.
00:11:21.760
For our examples, we are going to use a simple Facebook wrapper. The purpose here is to send a GET request to the Facebook Graph API for some basic user data, which includes the Facebook username, name, ID, and a few other fields. Below, you’ll see the testing case where we're setting up an expectation that our user ID matches Arjun’s user ID, since he created the Facebook Graph API wrapper.
00:12:06.479
In the example, we stub the request much the same way we would stub an object. We define the HTTP request method and provide the request link as a second argument. Next, we configure what the stub will return. This is an HTTP response we're crafting to return a status code, along with any headers, which is crucial if you plan on performing operations with the headers.
00:12:42.960
The body will include a simple JSON string that I've truncated for brevity. This straightforward test passes since we're performing no direct network requests. The reasons for stubbing requests in this manner include a reduction of time spent and the elimination of the intermittent failures we’ve discussed regarding network requests.
00:13:24.320
As a more advanced technique, several popular libraries for API wrappers also provide mock services that can simplify your testing process. I recommend taking advantage of these before defaulting to WebMock, as they can save you a lot of time.
00:13:51.440
Let’s look at a quick example of using Facebook Graph Mock. In this instance, we're setting up our spec helper to include methods from the library we want to test against. Our setup involves wrapping the test case with the appropriate calls to mock the request. We send a GET request to a specific endpoint, and the third parameter indicates where to find the JSON response file.
00:14:29.440
You can also create your own responses, which is a good practice, especially considering I found some issues with outdated responses from the Facebook Graph Mock. However, many of these libraries may provide pre-recorded responses for you, so you wouldn’t have to go through the effort of collecting these on your own. This methodology streamlines the process of testing APIs.
00:15:16.960
Another tool I want to introduce is Shamrack, which I enjoy using because it allows you to mount rack-based apps (like Rails and Sinatra) and makes requests against these fake apps. This leverages the power of Sinatra to stub out the endpoints effectively.
00:15:59.760
In our setup for Shamrack, we specify the endpoint we want to target (for example, graph.facebook.com) and include it within a Sinatra app. This flexibility allows you to create a response string or dynamically populate responses based on specific criteria, enhancing the depth of your testing.
00:16:39.600
Using this dynamic approach gives a more expressive and readable structure to your tests compared to some of the more rigid approaches with tools like WebMock. You can add as much functionality as needed to test your integrations thoroughly, making it much easier to discern where your API requests are being directed.
00:17:17.440
Next, I want to discuss the VCR gem, which is widely used and provides significant benefits. It allows you to pre-record your API interaction responses. It saves these in a cassette library, which stores your responses, enabling you to test without needing a live connection.
00:17:50.640
When using VCR, you’ll include a configuration block within your spec helper to set up your cassette library. The interaction is similar to the previous examples, where you wrap your requests in VCR. It will connect to the network, make a request on your behalf, and store the response for future tests.
00:18:22.320
This method alleviates the need to collect responses manually, which can be tedious and error-prone. With VCR, you simply run the tests, and it will capture all the necessary JSON responses for playback during your automated tests. This means you can run your test builds on CI servers without the potential for network failures disrupting the process.
00:19:05.920
Additionally, if you’re dealing with APIs lacking versioning (like the NHL example I discussed), consider building a process that can compare current responses to previously recorded versions, allowing you to catch changes in the API before they break your application.
00:20:07.360
There are also worth exploring tools like Puffing Billy, which focuses on in-browser requests. It can record and reuse requests similarly to VCR, making it another tool for your toolkit.
00:21:13.440
I want to make it clear that all these processes don't need to be confined to Ruby; there are many tools available that can help you collect and test API endpoints effectively. One of these tools is Chrome DevTools, which provides a user-friendly interface for monitoring requests and responses.
00:21:52.080
Another useful tool is Postman, which acts as an extension within Chrome that facilitates a user-friendly environment for exercising requests. It allows you to batch requests, save responses, and analyze the time taken for each request.
00:22:28.320
If you prefer working in the command line, consider using HTTPi, which offers an easier-to-use alternative to cURL, particularly for running scripts. Lastly, I recommend exploring Charles, which captures requests between your machine and network, facilitating insights into what requests are being made from devices and providing a resource for debugging.
00:23:17.120
As we wrap up, I want to highlight the importance of keeping track of your findings and strategies. I’ll be sharing the slides from today on Twitter, so feel free to connect. We’ve discussed the what, why, and how of testing services.
00:24:03.600
Testing services is essential as they comprise significant functionality. Skipping tests can be quite risky. Know your options—whether it's stubbing using Webmock, Shamrack, or Puffing Billy—and good luck determining the kind of flexibility you need versus the extra work that may be required.
00:24:43.200
Recording responses will certainly save you time in the long run, and I wish I had started doing this sooner; it’s incredibly useful. After me, I'd recommend sticking around for Austin's talk, which complements mine by delving deeper into issues with inconsistent test failures. If time permits, I encourage you to stay for that session.
00:25:21.440
Thank you for taking the time to listen to my talk. If I don't get a chance to answer your question today, please feel free to reach out via email or find me on Twitter. I truly appreciate your attention.