Three reasons to avoid using real web APIs in your integration tests
11th Jun 2020
Using real web APIs, like REST, gRPC, and GraphQL, can be appealing when you're testing your app. It allows you to create an environment as close to production as possible, which can give you high confidence that things will work as expected when they're live.
However, there are some good reasons not to use web APIs when testing. I'll go over three of those reasons, motivated by real-world examples, and give you tips on how to avoid these issues while still creating effective tests.
If you're a user of Google Cloud Platform, you may have created credentials that allow your app to authenticate against Google's services. While these credentials are useful to get up and running fast, they wind up tightly coupling your codebase to Google Cloud Platform in ways you do not expect. In this section, we'll explore how that tight coupling may create a security vulnerability in your app.
For example, imagine you are using Google's Key Management Service, or KMS, in a NodeJS app. Google's documentation shows that importing and using kms is relatively straightforward, requiring less than fifteen lines of code. What they do not tell you is that the line
require('@google-cloud/kms') triggers several automatic web calls, some of which attempt to authenticate with Google Cloud Platform. If you don't have a valid credential in your project, the authentication will fail and your code won't run. This seems like a pretty hefty price to pay for a
Thankfully, most libraries don't have these sorts of side effects when imported. But, because of this side effect, it requires a Google credential to be present in a project every time you test code that calls
require('@google-cloud/kms'). If this line is at the top-level of your app, it could mean that almost all your tests need to include this file and authenticate against Google's services.
Imagine now who is testing your code. You may have local unit tests on developers' machines. You may have tests running in a CI/CD environment. You may have a QA firm or third party contractor (like Meeshkan!) testing your app. Now, you need to distribute your private Google Cloud key to all of these parties in a secure manner. This poses two forms of challenges:
- Distribution - some enviroments, like CI/CD, require extra steps to get credentials in the right form (ie a file on a file system or an environment variable), which adds complexity.
- Maintenance - knowing where the credential is, who has access to it, and under what conditions it is protected.
Challenges in both distribution and maintenance increase the risk that a credential will leak. It also increases the risk that a stale or incorrect credential will be used.
A great way to avoid all of these problems is to not call APIs from your code that requires sensitive credentials. In the case of Google and similar providers, I've solved this problem in three ways:
- Gate imports with environment variables: Make sure that third-party libraries like
@google-cloud/kmsare only imported in an enviornment that is supposed to have credentials by using statements like
const kms = process.env._USE_GCP_LIBRARIES ? require('@google-cloud/kms') : require('./mock-kms').
- Use dependency injection: Refactor code so that there are very few top-level calls to
process.env. Instead, pass things like environment variables to your functions as arguments. While this will still require some form of gating between test and other environments, it limits the gating to one place.
- Use a mocking library like HMT or Unmock: This allows you to intercept HTTP traffic and create stubs for API calls to sensitive methods. As mocking tools become more mature and are able to handle GraphQL and gRPC, I believe this will be the most sustainable option going forward as it doesn't require changes to codebases.
At Meeshkan, we use 8base as our database and our API. Now some of you may say: "That's OUTRAGEOUS! You're giving your clients programmatic access to your database?" And to that, I say: "Yes."
I mean, why not? Projects like Prisma have been closing the gap between a database and an API for several years already. 8base is one of many companies that are pushing this approach to greater heights.
The speed and ease of cutting out the middleman between your API and database comes at a cost, though: it is more difficult to create a level of indirection between your API calls and database read/writes. In a more traditional setup where you are calling an API that then calls a database, you usually have a staging environment or mock of that API that diverts from the production database. Here, that isn't an option, which makes testing tricky.
At Meeshkan, the way we've solved that in our webapp is using a mock of GraphQL request to divert calls from 8base to a Jest mock function. It allows us to write tests for our app's most sensitive database calls, like creating or deleting projects, without fear of accidentally pinging 8base with one of our own credentials.
Network calls are slow. Even in the slowest of modern programming languages, an in-memory function call for a function that has no I/O and no heavy-duty algorithms (like FFTs) usually takes no more than 10ms. By contrast, the fastest network call to an external service will be at least 100ms and often take much longer. This problem, compounded over 100s of network calls in tests, can be the difference between a test suite that executes in 30 seconds or 30 minutes. Furthermore, if an external service is behaving erratically for any reason, it could fail your tests even though there's nothing wrong with your code.
Using network mocking tools like HMT or Unmock are great ways to solve this problem. The nice thing about this approach is that it can be adopted on a test-by-test basis, so you don't have to go all-in on mocking.
We've published quite a few articles on mocking on our blog. If you feel your tests are laggy, I'd invite you to check them out:
If you're reading this and thinking, "That's all well and good, but who even has time to think about this stuff? I don't have any tests yet and don't plan on having them anytime soon" - you're not alone!
That's the case for most developers and that's why we built Meeshkan. We wanted to build high-quality, automatic testing for services that rely on network calls for things like database connections and REST APIs. So, while I'd encourage you to adopt some of the practices above the next time you get into a security, integrity, or network-speed snag with your tests - I also encourage you to check out Meeshkan. Then you can see the extent to which high-quality automated testing can solve some of these problems for you.
Thanks, and happy testing!
Don’t miss the next post!
Absolutely no spam. Unsubscribe anytime.