Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add: Functional tests suite #1525

Merged
merged 15 commits into from
Nov 22, 2023
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"prettier": "prettier --config .prettierrc.json --write '**/**/**/**/*.js' '**/**/**/*.js' '**/**/*.js' '**/*.js' '*.js'",
"prettier:text": "prettier 'README.md' 'doc/*.md' 'doc/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always",
"test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 8000 --ui bdd --exit --color true",
"test:functional": "nyc --reporter=text mocha --recursive 'test/functional/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true",
"test:expression": "nyc --reporter=text mocha --recursive 'test/unit/expressions/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true",
"test:multientity": "nyc --reporter=text mocha --recursive 'test/unit/ngsiv2/plugins/multientity-plugin_test.js' --reporter spec --timeout 5000 --ui bdd --exit --color true",
"test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit",
Expand All @@ -58,6 +59,9 @@
"uuid": "~8.3.2"
},
"devDependencies": {
"async-mqtt": "~2.6.3",
"chai": "~4.3.10",
"chai-match-pattern": "~1.3.0",
"coveralls": "~3.1.1",
"eslint": "~8.18.0",
"eslint-config-tamia": "~8.0.0",
Expand Down
372 changes: 372 additions & 0 deletions test/functional/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,372 @@
## Functional test suite
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test/functional/README.md file should be linked in the root README.md file, from the right place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added here: 05b86a5


This directory contains the functional test suite for the IoTA JSON. This test suite is based on mocha and chai. For
mocks, we use the nock library. Additionally, it uses some specific functions to ease to implement the test. Helper
functions are located in the `testUtils.js` file.

There are 2 tests files in this directory:

- `fuctional-tests.js`: This file contains the test defined in the "classic way". This means, coded in the JS file as
any other mocha test. It uses the functions defined in the `testUtils.js` file to simplify the tests.
- `functional-tests-auto.js`: This file contains the test defined in the "automatic way". This means, the test cases
mapedraza marked this conversation as resolved.
Show resolved Hide resolved
are defined as JSON in a separate file (`testCases.js`). This file is loaded by the test suite and the test cases
are automatically generated. This is the recommended way to define the test cases.

mapedraza marked this conversation as resolved.
Show resolved Hide resolved
### Automatic test cases

Each test case is defined as a JSON object in the `testCases.js` file. This file is loaded by the test suite and the
test cases are automatically generated. Each test case is defined as an object with the following elements:

- `describeName`: The name of the `DESCRIBE` test case. This will be used to generate the test case name in the mocha
fgalan marked this conversation as resolved.
Show resolved Hide resolved
test suite.
- `provision`: The JSON object that will be sent to the IoTA JSON provisioning API. This will be used to create the
group. It contains the following elements:
- `url`: The URL of the provisioning API (group)
- `method`: The HTTP method to use (POST)
- `json`: The JSON object that defines the group
- `headers`: The headers to send to the provisioning API. This should contain the `fiware-service` and
`fiware-servicepath` headers.
- `skip`: optional. If set to `true`, the test case (`describe`) will be skipped. This is useful to skip test
cases that are not supported by the agent. It can also have a string value `lib`. This will skip the test case
mapedraza marked this conversation as resolved.
Show resolved Hide resolved
only if the is executed in the IoTA Node lib repo. This is useful to skip test cases that are not supported by
the lib (I.E: all tests related to the transport).
- `should`: The array of test cases to execute. Each test case is defined as an object with the following elements:
- `transport`: The transport to use to send the measure. This can be `HTTP` or `MQTT`. It uses `HTTP` by default
or if the `transport` element is not defined. See the "Advanced features" section for more information.
- `shouldName`: The name of the `IT` test case. This will be used to generate the test case name in the mocha test
suite.
- `type`: The type of the test case. This can be `single` or `multientity`. See the "Advanced features" section
for more information.
- `measure`: The JSON object that will be sent to the IoTA JSON measure API. This will be used to send the
measure. It contains the following elements:
- `url`: The URL of the measure API (group)
- `method`: The HTTP method to use (POST)
- `qs`: The query string to send to the measure API. This should contain the `i` and `k` parameters.
- `json`: The JSON object that defines the measure
- `expectation`: The JSON object that defines the expectation. This will be used to check that the measure has
been correctly sent to the Context Broker.
- `loglevel`: optional. If set to `debug`, the agent will log all the debug messages to the console. This is
useful to check the messages sent to the Context Broker. See the "Debugging automated tests" section for more
information.
- `skip`: optional. If set to `true`, the test case (`it`) will be skipped. Same as the `skip` element in the
`provision` element.
- `isRegex`: optional. If set to `true`, then the expectation will be treated as a regular expression. This is
useful to check that the measure has been correctly sent to the Context Broker when the measure contains a a
variable parameter like a timestamp. See the "Advanced features" section for more information.

#### Example

```javascript
{
describeName: 'Basic group provision with attributes',
provision: {
url: 'http://localhost:' + config.iota.server.port + '/iot/services',
method: 'POST',
json: {
services: [
{
resource: '/iot/json',
apikey: '123456',
entity_type: 'TheLightType2',
cbHost: 'http://192.168.1.1:1026',
commands: [],
lazy: [],
attributes: [
{
object_id: 's',
name: 'status',
type: 'Boolean'
},
{
object_id: 't',
name: 'temperature',
type: 'Number'
}
],
static_attributes: []
}
]
},
headers: {
'fiware-service': 'smartgondor',
'fiware-servicepath': '/gardens'
}
},
should:[
{
shouldName: 'should send its value to the Context Broker',
config:{
type: 'single'
},
measure: {
url: 'http://localhost:' + config.http.port + '/iot/json',
method: 'POST',
qs: {
i: 'MQTT_2',
k: '123456'
},
json: {
s: false,
t: 10
}
},
expectation: {
id: 'TheLightType2:MQTT_2',
type: 'TheLightType2',
temperature: {
value: 10,
type: 'Number'
},
status: {
value: false,
type: 'Boolean'
}
}
},
{
transport: 'MQTT',
shouldName: 'should send its value to the Context Broker when using MQTT',
config:{
type: 'single'
},
measure: {
url: 'http://localhost:' + config.http.port + '/iot/json',
method: 'POST',
qs: {
i: 'MQTT_2',
k: '123456'
},
json: {
s: false,
t: 10
}
},
expectation: {
id: 'TheLightType2:MQTT_2',
type: 'TheLightType2',
temperature: {
value: 10,
type: 'Number'
},
status: {
value: false,
type: 'Boolean'
}
}
}
]
}
```

### Advanced features

#### Multientity

This test suite support the multientity feature. To test this feature, you need to set add to the test case the
parameter `type: 'multientity'`. This will automatically take care of the multientity feature. This means that the suite
will configure the mock to listen to `/v2/op/update` instead of `/v2/entities?options=upsert`.

In particular, it will configure the mock to listen the correct URL. You should define the expectation for the test case
as a batch operation (see the following example).

```javascript
{
"entities": [
{
"id": "TheLightType2:MQTT_2",
"type": "TheLightType2",
"status": {
"value": false,
"type": "Boolean"
}
},
{
"id": "TheLightType2:MQTT_3",
"type": "TheLightType2",
"temperature": {
"value": 10,
"type": "Number"
}
}
],
"actionType": "append"
}
```

#### Multimeasures

It is also supported to test cases in which is sent more than one measure. To do so, you need to define the test case
expectation as an array, with one object for each measurement. Then, the suite will recognize the array length and will
mapedraza marked this conversation as resolved.
Show resolved Hide resolved
expect the same number of NGSI requests. I.E:

```js
[
{
id: 'TheLightType2:MQTT_2',
type: 'TheLightType2',
temperature: {
value: 10,
type: 'Number'
},
status: {
value: false,
type: 'Boolean'
}
},
{
id: 'TheLightType2:MQTT_2',
type: 'TheLightType2',
temperature: {
value: 20,
type: 'Number'
},
status: {
value: true,
type: 'Boolean'
}
}
];
```

You also should define the measure as multimeasure. This is done by defining the measure JSON element as an array of
mapedraza marked this conversation as resolved.
Show resolved Hide resolved
objects. Each object will be a measure that will be sent to the Context Broker in a different request. I.E:

```javascript
measure: {
url: 'http://localhost:' + config.http.port + '/iot/json',
method: 'POST',
qs: {
i: 'MQTT_2',
k: '123456'
},
json: [
{
s: false,
t: 10
},
{
s: true,
t: 20
}
]
}
```

#### Transport

The test suite supports using the internal node lib function `iotAgentLib.update`, `HTTP` or `MQTT` for measure sending.
In order to select the specific way to send the measure you should add a `transport` element to each should case having
the value set to `MQTT`. By doing so, the suite will automatically configure the mock to connect to the MQTT broker and
send the measure to the correct topic based on the `i` and `k` parameters. It will ignore the `url` and `method`
parameters present in the measure JSON element. I.E:

```javascript
should: [
{
transport: 'MQTT',
shouldName: 'should send its value to the Context Broker when using MQTT',
type: 'single',
measure: {
url: 'http://localhost:' + config.http.port + '/iot/json',
method: 'POST',
qs: {
i: 'MQTT_2',
k: '123456'
},
json: {
s: false,
t: 10
}
},
expectation: {
id: 'TheLightType2:MQTT_2',
type: 'TheLightType2',
temperature: {
value: 10,
type: 'Number'
},
status: {
value: false,
type: 'Boolean'
}
}
}
];
```

#### No payload reception

The test suite also supports the case in which the Context Broker does not receive any payload. This is done by defining
the expectation as an empty object. I.E:

```javascript
...
expectation: []
...
```

mapedraza marked this conversation as resolved.
Show resolved Hide resolved
### Debugging automated tests

It is possible to debug the automated tests by using the loglevel parameter set to `debug` for each should case. This
parameter configures the IoTA log level. By setting it to `debug`, the agent will log all the debug messages to the
console. This is useful to check the messages sent to the Context Broker.

It is also useful to debug the test by adding a breakpoint in the `testUtils.js` (read the comments in the file to know
where to add the breakpoint). This will allow you to debug just that particular test case stopping the execution in the
breakpoint.

Example of a test case with the loglevel set to `debug`:

```javascript
should:[
{
shouldName: 'should send its value to the Context Broker when using MQTT',
loglevel: 'debug',
transport: 'MQTT',
config:{
type: 'single'
},
measure: {...},
expectation: {...}
}
]
```

### Using expressions in the expectation

It is possible to use expressions in the expectation. This is useful to check that the measure has been correctly sent
to the Context Broker when the measure contains a variable parameter like a timestamp. To do so, you need to set the
`isRegex` parameter to `true` in the test case. This will tell the test suite that the expectation can contain some
expressions. The expression support is based on [Chai match pattern plugin](https://github.com/mjhm/chai-match-pattern).
This plugin relies on [loadash match pattern](https://github.com/mjhm/lodash-match-pattern) to support the expressions.
For further information about the expression support, check the documentation of the previous links.

Even if setting the `isRegex` parameter to `true` make the test pass if the expectation does not contain any expression,
it is recommended to do not set this parameter to `true` if the expectation does not contain any expression because the
test result will be less clear if fails (it will show the error message of the expression library instead of the
expected value).

Here, you can find an example of a test case using expressions:

```javascript
expectation: {
id: globalEnv.entity_name,
type: globalEnv.entity_type,
attr_a: {
value: 21,
type: 'Number',
metadata: {
TimeInstant: {
type: 'DateTime',
value: _.isDateString
}
}
},
TimeInstant: {
type: 'DateTime',
value: _.isDateString
}
}
```

The previous example will check that the `TimeInstant` value and `attr_a.metadata.TimeInstant.value` are valid date.
Loading
Loading