Dependency inversion is an extremely powerful technique that works not only with functions, but also with objects. Let's take a deeper look at it using HTTP requests as an example, and get acquainted with the notion of a stub.
Suppose we have a function that parses an organization's private repositories on GitHub and returns the ones that are forks (i.e., split off from the main directory):
// Library for working with GitHub API
import Octokit from '@octokit/rest';
const getPrivateForksNames = async (username) => {
const client = new Octokit();
// The client makes a request to a GitHub and returns a list of the specified organization's private repositories
// The data is stored in the data property of the return response
const { data } = await client.repos
.listForOrg({
username,
type: 'private',
});
// We leave only the names of the forks
return data.filter(repo => repo.fork).map(repo => repo.name);
};
Let's test it. What do we want from this feature? First of all, we need to make sure that it works correctly, i.e., that it returns an array of private forks. An ideal test would look like this:
test('getPrivateForksNames', async () => {
const privateForks = await getPrivateForksNames('hexlet');
expect(privateForks).toEqual([/* an array of names that we expect to see */]);
});
Unfortunately, it's not that simple. An HTTP request is made inside the function. Let's see what kind of problems this can cause:
- An unstable network can slow down test execution and lead to phantom errors. The tests will sometimes past, and sometimes not.
- Services like github.com have limits on requests per second, per hour, per day, and so on. It's guaranteed that the tests will begin to stall at these limits. Moreover, there's a chance that the machine that the requests come from will be blocked.
- The actual data on GitHub isn't static, it can and probably will change, which again will lead to errors and your tests will need to be fixed.
In this example, the HTTP request is seen as a hindrance to testing our underlying logic. We trust github.com and its @octokit/rest library, which means we don't need to check it works correctly (or you'll go nuts if you don't trust anyone).
In the previous lesson, we learned about several ways out of this situation, this is one instance in which we can apply one of them.
Dependency inversion
In order to use dependency inversion, we add the Octokit client itself as the second argument for the function. This will allow you to substitute it in the tests:
import Octokit from '@octokit/rest';
// The library is passed from the outside and can substituted
const getPrivateForksNames = async (username, client = new Octokit()) => {
// ...
};
We have to implement a fake client, which behaves similarly to the real Octokit except that it doesn't make any network requests. We also need to describe the specific data that the listForOrg call will return. Only then we can test to see that the getPrivateForksNames()
function works correctly.
// The structure of this class only describes the part
// needed to call await client.repos.listForOrg(...)
class OctokitFake {
// Here we define the desired data that is to be returned in the test
constructor(data) {
this.data = data;
}
repos = {
listForOrg: () => {
// the structure of the return must match the real client
// Only then will it be possible to transparently replace the real client with a fake one
return Promise.resolve({ data: this.data }); // because the method is asynchronous
},
}
}
And the test itself using this client:
import OctokitFake from '/OctokitFake.js';
test('getPrivateForksNames', async () => {
const data = /* the response from GitHub, which we want to verify */;
const client = new OctokitFake(data);
// Internally, a "query" is performed that returns the data generated above
const username = /* GitHub username */;
const privateForks = await getPrivateForksNames(username, client);
expect(privateForks).toEqual(/* which we expect based on what listForOrg returned */);
});
When it comes to testing for such fake objects (or functions), we have a special name for them - stubs. A stub replaces a real object or function, avoiding side effects or making the code deterministic. Stubs aren't used to test anything, they only allow you to isolate the part that interferes with testing the underlying logic.
Banning HTTP requests
Another way to avoid HTTP requests from tests is to disable them in tests. In future lessons, we'll get acquainted with the Nock library, which has a method for banning any HTTP requests from the code: nock.disableNetConnect()
. We recommend calling it at the beginning of the test file. In addition, it helps to see which pages are being queried by third-party libraries. This is what the output looks like after disabling external connections (assuming that no query spoofing was performed):
HttpError: request to https://api.github.com/orgs/hexlet/repos?type=private failed, reason: Nock: Disallowed net connect for "api.github.com:443/orgs/hexlet/repos?type=private"
Are there any more questions? Ask them in the Discussion section.
The Hexlet support team or other students will answer you.
For full access to the course you need a professional subscription.
A professional subscription will give you full access to all Hexlet courses, projects and lifetime access to the theory of lessons learned. You can cancel your subscription at any time.