00:00:14.480
Today, I'm going to be discussing testing integrations: the good, the bad, and the ugly. I chose the title before I had any content prepared because it kind of rolls off the tongue, and people recognize the movie; I have not watched it myself. I hope everything fits together.
00:00:27.599
If you saw my talk at Rails World last year and enjoyed the animations, I hope this one does not disappoint! My name is Julia López, and I'm from Barcelona. I've been working with Rails since 2011. I love refactoring, upgrades, and everything that Robi mentioned before. I'm always pushing for that to happen.
00:00:50.760
I've added something new to my repertoire called billing, because for the last three years, I've been leading the implementation of a new billing system at Harvest, specifically integrating with Stripe. Just a heads-up: most of the examples I'll cover today will be about that because I wanted to focus more on the present rather than the past.
00:01:12.200
I work at Harvest, the time tracking tool, which has been built on Rails since 2006. We have a small engineering team, so I have had the chance to work on many parts of the code. It's a monolith, and we own everything; it's our baby, and even with an 18-year-old app, that's something to keep in mind when I go through examples today.
00:01:51.840
I want to thank the people who set up everything I’m going to show today. I have just been using it and extending it, but it wasn't my original idea. Harvest connects to multiple tools to extend the features we offer, like QuickBooks, Stripe, Microsoft, Google APIs, PayPal, Slack, and others for internal use like Stripe, Braintree, and CustomerIO.
00:02:23.360
I wanted you to understand that working with APIs is not always joyful; we've seen it all. So, what can you expect from my talk? This is not a discussion about why testing is important—it is important, as Robi mentioned—but rather a practical insight into popular Ruby gems for testing. My recommendations will be opinionated, and I'll show you the pros and cons through real-life examples.
00:02:59.839
I hope you leave knowing something new. You might already be aware of some topics, but I believe you will relate to our experiences and challenges. Let's start by refreshing the concepts for those who may not know: what is an integration? A software integration is connecting software with another piece via Application Programming Interfaces (APIs). APIs are the rules and protocols that allow software to communicate with each other.
00:03:51.840
There are many APIs, with the most familiar being HTTP and HTTPS. All examples today will deal with APIs that operate on these standards. We will specifically focus on third-party APIs. You could have your own front-end client with your own API, but that's not what we're discussing here. Testing, a broad term, refers to checking whether your software works as intended. We will specifically look into automated testing.
00:04:49.639
Automated testing means having software that tests your software—it's checking that your software performs as predicted. Why focus on testing external API integrations instead of general testing? Because you can conduct general testing with the tools I will discuss, but we cannot control third-party APIs. There are many issues to consider, and developers rarely express joy when working with these APIs.
00:05:40.319
Writing tests is challenging, especially for external integrations. Let's start with a simple billing integration example using the Stripe API for subscription creation. Imagine a service that will make a POST request to the subscription endpoint of the Stripe API. The subscription endpoint returns a response with various attributes about the created subscription.
00:06:27.720
In a simple code example, you have a create method that generates a subscription. However, where is the API call located? To be realistic, we're using the Stripe gem to implement our billing integration, which I highly recommend, but as Robi said, be cautious about adding too many gems to your stack.
00:07:05.560
If your use case involves only one API call, it's better to avoid unnecessary gems. Instead, utilize an HTTP client like Faraday whenever possible. For the sake of this presentation, let's pretend everything is functioning perfectly. Testing is essential when building an integration, so how do we approach this testing? There are different approaches, but remember that not testing is not an option. If you want to take risks, then that's a separate approach.
00:08:03.159
One approach involves making a live API call for every test run; that’s acceptable, but it can lead to complications. Let's look at some code demonstrating this concept. Here's a simple test definition using MiniTest to call the service and assert expectations. However, your test suite will likely be more complex than this example, especially when dealing with something as nuanced as a billing system.
00:09:25.480
You can easily accumulate hundreds of tests to run, quickly escalating in size. Consequently, you may encounter problems, such as slow API calls. For instance, I benchmarked the time it takes to invoke our service, and it took 400 milliseconds just to hit the API. No one enjoys a slow continuous integration process, especially when things are urgent.
00:10:40.319
Another problem is rate limiting, which many third-party APIs impose to prevent service overload. Stripe, for instance, has strict rate limits, especially in test mode. Finally, you also need to account for potential issues like poor network conditions, timeouts, latency, or interruptions due to maintenance or human error. Even the best services occasionally experience problems.
00:11:40.680
Another concern is fluctuating data. If you're using a production API for testing, often data will change, creating instability within your tests. What happens if a customer or pricing plan disappears? Your tests may break, hindering your development process.
00:12:12.080
Then there are issues related to authentication and authorization, especially if you are using OAuth flows which require token management. More API requests can lead to slower tests, and if you exceed limits, errors can arise. Then we have to ask, when is it safe to deploy?
00:12:33.120
As you can see, many things can go wrong. The solution? Avoid hitting APIs directly and instead use stubs and mocks. A mock is an interceptor in your tests that prevents API calls from executing. For example, if you're using `Mocha`, a popular Ruby library for mocking and stubbing, you would set expectations that your service calls the API using predetermined requests and responses.
00:13:23.680
Let’s revisit the previous Stripe example. If we employed the Stripe gem directly, we would mock the Stripe subscription method to set expectations and simulate responses. If we use `WebMock`, it allows setting expectations on HTTP requests without being limited to a specific client, which is beneficial for flexibility.
00:14:51.360
Utilizing `WebMock`, you can stop and set expectations on HTTP requests by specifying parameters. You can configure your tests to assert the body and response of the requests made, ensuring your integration behaves as expected. With both `WebMock` and `Mocha`, we ensure that our tests are not tied to live API calls, leading to faster and more stable test runs.
00:15:56.960
But what happens if I only need certain attributes? For instance, if Stripe returns 40 attributes, but I’m only mocking a few, how can I accommodate various scenarios like different subscription types? You end up needing a lot of boilerplate code to represent different states, especially since API responses can change.
00:16:50.480
What if I tell you that there's a better solution? Enter `VCR`, a gem that records your HTTP interactions during tests and replays them in future test runs. This makes it easy to manage our API calls by preventing unnecessary duplicates while also keeping our tests consistent.
00:17:53.240
When you utilize VCR, you need to tell your tests to use a cassette—a recorded file of your real HTTP calls. This way, the first time you run your test, it will hit the API, and subsequent runs will replay that recorded response, effectively eliminating repetitive API calls in many cases.
00:18:56.480
VCR's recordings preserve a substantial amount of information, including status codes and request details, but it is also crucial to ensure that customer IDs and price IDs used are valid and relevant, as VCR will re-use your cassettes at runtime. This allows us to work efficiently without additional overhead from API calls.
00:19:57.680
While VCR and `WebMock` work well together, the challenge is that we are managing a 17-year-old codebase here at Harvest, which makes some integrations difficult to modernize quickly. Nevertheless, VCR and `WebMock` can provide insight and ease in testing our API interactions.
00:20:25.240
Our VCR configuration is essential; it sets the folder where cassettes are saved and integrates with other libraries like `WebMock` to ensure that our tests remain inside expected behaviors without unexpected calls to outside services, especially during continuous integration.
00:21:11.960
In terms of maintaining a clean testing environment, we need to keep an eye on our VCR cassettes. For instance, when a major change happens, we need to recreate these cassettes, which can lead to significant file changes. This is an annoyance during pull requests, as they often come with a multitude of cassette changes.
00:21:55.880
Even though coding more tools might sound overwhelming, it actually streamlines our tests because VCR helps us avoid unnecessary API interactions. With proper documentation, new developers can acclimate quickly to necessary setups for authentication and token management. Challenges may arise, but so long as we pay attention, the setup remains effective.
00:22:46.720
Incorporating tools like `WebMock` and `VCR` helps us test integrations realistically while maintaining clarity in request and response visibility. We also have the flexibility to adjust our systems based on output without managing the entire system call logic. This is vital for maintaining and extending functionalities effectively, leading to better test suites.
00:23:47.480
While we have shared the challenges of working with APIs, from fluctuating data to complex setups, thorough testing ensures a smooth deployment pipeline. Keeping in mind the importance of effective testing will have a serious positive impact on our products and processes, encouraging faster iterations and more robust software.
00:24:34.760
To wrap things up, thorough involvement with both VCR and WebMock benefits us in multiple areas—from audit trails of API interactions to significant boosts in our testing speed. Remember that every integration will have its own unique complexities, but utilizing tools strategically will power through hurdles without sacrificing performance or reliability.
00:30:05.760
Thank you!