RailsConf 2014

Effectively Testing Services

Effectively Testing Services

by Neal Kemp

In his talk at RailsConf 2014, Neal Kemp addresses the challenges of testing services in the Ruby programming environment, where applications frequently interact with external APIs such as Twitter and AWS, alongside internal services. This session focuses not only on the necessity and methods of testing these services but also on the common pitfalls developers might encounter.

Key Points Discussed:

  • Introduction to Services: Kemp categorizes services into two types: external services (like public APIs) and internal services (where HTTP requests are made to other repositories). He emphasizes services' critical roles in scaling and speeding up app development.

  • Importance of Testing Services: Testing is framed as essential because services often constitute important features within an application, such as payment processing or data retrieval. An untested service can lead to significant application failures (e.g., a malfunctioning Stripe integration affecting billing processes).

  • Challenges From Real-World Examples: Kemp shares personal anecdotes regarding difficulties faced while working with both internal APIs, which suffered from inconsistent responses, and external APIs, which lacked proper documentation and versioning. He underscores these issues with examples including undocumented bugs that caused failures during production.

  • Best Practices for Testing Services:

    • Airplane Mode Analogy: He advises to 'turn on airplane mode' in tests, meaning no real network requests should be sent during testing. This helps avoid unreliable tests due to network failures.
    • Stubbing Techniques: Kemp discusses the need to stub out requests using libraries like WebMock, providing a way to simulate API calls without making actual network requests. He shares a simple implementation using RSpec and how to easily set up and configure the testing environment.
    • Using Mock Services: Recommendations include leveraging existing gems that include mock services or employing tools like VCR, which records HTTP interactions for reuse in tests, eliminating the need for real requests during builds.
    • Dynamic Testing with Shamrack: He explains Shamrack for creating dynamic mock services, allowing greater flexibility in testing responses.
    • Conclusion and Recommendations: Testing services is crucial in modern development workflows, and developers should determine the best approach based on project needs, emphasizing the importance of recording responses and avoiding the pitfalls of external dependencies.

Takeaways:

  • Developing confidence in service interactions through thorough testing can lead to more reliable applications.
  • The right tools and techniques can streamline service testing processes, helping developers efficiently quality-check their integration points.
  • The audience is encouraged to stay informed about future best practices, emphasizing the importance of maintaining test reliability.

Kemp closes by inviting further discussion through email or social media, indicating his openness for question and collaborative conversation on the topic.

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.