Smooth mocking with the Elasticsearch Node.js client
A classic problem that every backend developer has faced during their work is testing an application that uses a database. A perfectly valid solution is to use the real database for testing your application, but you would be doing an integration test, while you want a unit test.
There are many ways to solve this problem. You could create the database with docker, or use an in-memory compatible one, but if you are writing unit tests that can be easily parallelized this will become quite uncomfortable. A different way of improving your testing experience while doing unit tests is to use a mock.
What is a mock?
The Cambridge dictionary defines a mock as follows: not real but appearing or pretending to be exactly like something.
In software, mocking is often used when writing unit tests, as an API you have written may have dependencies on other complex parts of your infrastructure, such as a database. To isolate the behavior of an API you need to replace external components with mocks that will simulate the behavior of real components.
Mocking Elasticsearch (and sleeping at night)
The client you use for connecting to Elasticsearch is designed to be easy to extend and adapt to your needs. Thanks to its internal architecture it allows you to change some specific components while keeping the rest of it working as usual. Each Elasticsearch official client is composed of the following components:
- API layer: every Elasticsearch API that you can call
- Transport: a component that takes care of preparing a request before sending it and handling all the retry and sniffing strategies
- ConnectionPool: Elasticsearch is a cluster and might have multiple nodes; the ConnectionPool takes care of them
- Serializer: A class with all the serialization strategies, from the basic JSON to the new line-delimited JSON
- Connection: The actual HTTP library
The best way to mock Elasticsearch with the official clients is to replace the Connection component since it has very few responsibilities and it does not interact with other internal components other than getting requests and returning responses.
Writing a mock for your test each time can be annoying and error-prone, so we have built a simple yet powerful mocking library specifically designed for the Elasticsearch Node.js client, and you can install it with the following command:
npm install @elastic/elasticsearch-mock --save-dev
Introducing @elastic/elasticsearch-mock
With this library you can easily create custom mocks for any request you can send to Elasticsearch. It offers a simple and intuitive API and it mocks only the HTTP layer, leaving the rest of the client working as usual. Before showing all of its features, and what you can do with it, let’s see an example:
const { Client } = require('@elastic/elasticsearch') const Mock = require('@elastic/elasticsearch-mock') const mock = new Mock() const client = new Client({ node: 'http://localhost:9200', Connection: mock.getConnection() }) mock.add({ method: 'GET', path: '/' }, () => { return { status: 'ok' } }) client.info(console.log)
As you can see it works closely with the client itself. Once you have created a new instance of the mock library, you just need to call the mock.getConnection()
method and pass its result to the Connection
option of the client. From now on, every request will be handled by the mock library, and the HTTP layer will never be touched. As a result, your test will be significantly faster, and you will be able to easily parallelize them!
The library allows you to write both “strict” and “loose” mocks, which means that you can write a mock that will handle a very specific request or be looser and handle a group of requests. Let’s see this in action:
mock.add({ method: 'POST', path: '/indexName/_search' }, () => { return { hits: { total: { value: 1, relation: 'eq' }, hits: [{ _source: { baz: 'faz' } }] } } }) mock.add({ method: 'POST', path: '/indexName/_search', body: { query: { match: { foo: 'bar' } } } }, () => { return { hits: { total: { value: 0, relation: 'eq' }, hits: [] } } })
In the example above every search request will get the first response, while every search request that uses the query described in the second mock will get the second response.
You can also specify dynamic paths:
mock.add({ method: 'GET', path: '/:index/_count' }, () => { return { count: 42 } }) client.count({ index: 'foo' }, console.log) // => { count: 42 } client.count({ index: 'bar' }, console.log) // => { count: 42 }
And wildcards are supported as well.
Another very interesting use case is the ability to create a test that randomly fails to see how your code reacts to failures:
mock.add({ method: 'GET', path: '/:index/_count' }, () => { if (Math.random() > 0.8) { return ResponseError({ body: {}, statusCode: 500 }) } else { return { count: 42 } } })
Conclusions
We have seen how simple it is to mock Elasticsearch and test your application. There are many more features and examples in the module documentation, and many others will come in the future.
If you’d like to try it out, spin up a free 14-day trial of the Elasticsearch Service, or download today,
If you have any questions or feature requests, don’t hesitate to open an issue.
Happy testing!