Co-authored-by: Tomas Della Vedova <delvedor@users.noreply.github.com> Co-authored-by: Tomas Della Vedova <delvedor@users.noreply.github.com>
157 lines
4.9 KiB
Plaintext
157 lines
4.9 KiB
Plaintext
[[client-testing]]
|
||
=== Testing
|
||
|
||
Testing is one of the most important parts of developing an application.
|
||
The client is very flexible when it comes to testing and is compatible with
|
||
most testing frameworks (such as https://www.npmjs.com/package/ava[`ava`],
|
||
which is used in the examples below).
|
||
|
||
If you are using this client, you are most likely working with {es}, and one of
|
||
the first issues you face is how to test your application. A perfectly valid
|
||
solution is to use the real {es} instance 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 becomes quite uncomfortable. A different way of improving your
|
||
testing experience while doing unit tests is to use a mock.
|
||
|
||
The client 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 {es} official client is composed
|
||
of the following components:
|
||
|
||
* `API layer`: every {es} 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`: {es} 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 {es} 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.
|
||
|
||
|
||
[discrete]
|
||
==== `@elastic/elasticsearch-mock`
|
||
|
||
Writing each time a mock for your test can be annoying and error-prone, so we
|
||
have built a simple yet powerful mocking library specifically designed for this
|
||
client, and you can install it with the following command:
|
||
|
||
[source,sh]
|
||
----
|
||
npm install @elastic/elasticsearch-mock --save-dev
|
||
----
|
||
|
||
With this library you can create custom mocks for any request you can send to
|
||
{es}. 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:
|
||
|
||
[source,js]
|
||
----
|
||
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 is handled by the mock library, and the HTTP layer will never be
|
||
touched. As a result, your test is significantly faster and you are 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 handles a very specific request or be looser and
|
||
handle a group of request, let’s see this in action:
|
||
|
||
[source,js]
|
||
----
|
||
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 gets the first response, while every
|
||
search request that uses the query described in the second mock gets the second
|
||
response.
|
||
|
||
You can also specify dynamic paths:
|
||
|
||
[source,js]
|
||
----
|
||
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:
|
||
|
||
[source,js]
|
||
----
|
||
mock.add({
|
||
method: 'GET',
|
||
path: '/:index/_count'
|
||
}, () => {
|
||
if (Math.random() > 0.8) {
|
||
return ResponseError({ body: {}, statusCode: 500 })
|
||
} else {
|
||
return { count: 42 }
|
||
}
|
||
})
|
||
----
|
||
|
||
We have seen how simple is mocking {es} and testing your application, you can
|
||
find many more features and examples in the
|
||
https://github.com/elastic/elasticsearch-js-mock[module documentation]. |