Skip to content

Commit

Permalink
Introduce k6 integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
a.e.aminova committed Jun 3, 2024
1 parent c19bdb5 commit 64a86c7
Show file tree
Hide file tree
Showing 22 changed files with 1,741 additions and 1 deletion.
18 changes: 18 additions & 0 deletions backend/integrationTests/k6/howToTest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# How to test with k6

k6 is used for integration testing.
At first start mockingbird app, then set up `httpHost` and `grpcHost` variables, after that run tests.

### Shell commands

- To create new test use `k6 new fileName.js`

- To run test script use `k6 run filename.js`

- To group several tests in a single run use scenarios (e.g. see file `scenario.js`).
To run scenario use `k6 run scenario.js`

### Links

- [Running k6](https://grafana.com/docs/k6/latest/get-started/running-k6/)
- [Javascript API](https://grafana.com/docs/k6/latest/javascript-api/)
31 changes: 31 additions & 0 deletions backend/integrationTests/k6/v2/definitions/test_service.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
syntax = "proto3";
package market_data;

enum InstrumentIDKind {
ID_1 = 0;
ID_2 = 1;
}

message PricesRequest {
string instrument_id = 1;
optional InstrumentIDKind instrument_id_kind = 3;
}

message PricesResponse {
enum Code {
OK = 0;
ERROR = 1;
}

string instrument_id = 1;
string tracking_id = 3;
Code code = 4;
optional string error = 100;
}

service OTCMarketDataService {
rpc PricesUnary (PricesRequest) returns (PricesResponse) {}
rpc PricesClient (stream PricesRequest) returns (PricesResponse) {}
rpc PricesServer (PricesRequest) returns (stream PricesResponse) {}
rpc PricesBidi (stream PricesRequest) returns (stream PricesResponse) {}
}
68 changes: 68 additions & 0 deletions backend/integrationTests/k6/v2/scenario.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { default as unaryV2Test } from './unary.js';

export const httpHost = 'http://localhost:8228/api/internal/mockingbird';
export const grpcHost = 'localhost:9000';

export function httpUri(path) {
return httpHost + path
}

export const httpOptions = {
headers: { 'Content-Type': 'application/json' }
}

export const serviceName = 'beta'


export const options = {
scenarios: {
unaryV2Scenario: {
executor: 'per-vu-iterations',
exec: 'unaryV2Scenario',

startTime: '0s',
gracefulStop: '5s',
tags: { connectionType: 'unary' },

vus: 1,
iterations: 1,
maxDuration: '5s',
},
},
};

export function setup() {
const serviceData = { name: serviceName, suffix: serviceName }
const serviceRes = http.post(httpUri('/v2/service'), JSON.stringify(serviceData), httpOptions)
}

export function unaryV2Scenario() {
unaryV2Test()
}

export function teardown() {

// delete all method descriptions and stubs for service
let methodDescriptionsRes = http.get(httpUri(`/v4/grpcMethodDescription?service=${serviceName}`))
let methodDescriptionIds = methodDescriptionsRes.json("#.id")
console.info(`methodDescriptionIds=${methodDescriptionIds}`)

for (let i in methodDescriptionIds) {
let methodDescriptionId = methodDescriptionIds[i]
let grpcStubsRes = http.get(httpUri(`/v4/grpcStub?query=${methodDescriptionId}`))
let grpcStubIds = grpcStubsRes.json("#.id")
console.info(`Alive grpcStubIds=${grpcStubIds}`)

for (let i in grpcStubIds) {
let stubId = grpcStubIds[i]
let grpcStubRes = http.del(httpUri(`/v2/grpcStub/${stubId}`))
console.info(`del stub: id=${stubId}; status=${grpcStubRes.status}`)
}

let methodDescriptionRes = http.del(httpUri(`/v4/grpcMethodDescription/${methodDescriptionId}`))
console.info(`del methodDescription: id=${methodDescriptionId}; status=${methodDescriptionRes.status}`)
}

}
72 changes: 72 additions & 0 deletions backend/integrationTests/k6/v2/unary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import encoding from 'k6/encoding';
import http from 'k6/http';
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';

import { serviceName } from './scenario.js'
import { httpHost, httpUri, httpOptions, grpcHost } from './scenario.js'
import { setup as scenarioSetup, teardown as scenarioTeardown } from './scenario.js'

const methodName = 'market_data.OTCMarketDataService/PricesUnary'

const grpcClient = new grpc.Client();
grpcClient.load(['definitions'], 'test_service.proto');

const protoFile = open('./definitions/test_service.proto');
const base64_proto = encoding.b64encode(protoFile);

export function setup() {
scenarioSetup()
}

export default function() {

const stubData = {
name: "unary v2 countdown stub",
labels: [],
scope: "countdown",
times: 1,
methodName: methodName,
requestClass: "PricesRequest",
requestPredicates:{},
responseClass: "PricesResponse",
response: {
"data": {
"code": "OK",
"instrument_id": "${req.instrument_id}",
"tracking_id": "${req.instrument_id_kind}"
},
"mode":"fill"
},
state: null,
seed: null,
service: serviceName,
requestCodecs: base64_proto,
responseCodecs: base64_proto
}
const stubRes = http.post(httpUri('/v2/grpcStub'), JSON.stringify(stubData), httpOptions)
check(stubRes, { 'create stub status': (r) => r.status === 200 })

grpcClient.connect(grpcHost, { plaintext: true })

const grpcReq = { instrument_id: 'instrument_1', instrument_id_kind: 'ID_1' }
const response = grpcClient.invoke(methodName, grpcReq)
const expectedMessage = { instrument_id: grpcReq.instrument_id, trackingId: grpcReq.instrument_id_kind, code : "OK" }
check(response, {
'call grpc stub - status is OK': (r) => r && r.status === grpc.StatusOK,
'call grpc stub - check response message': (r) => r.message && r.message.code === "OK" &&
r.message.instrumentId === grpcReq.instrument_id && r.message.trackingId === grpcReq.instrument_id_kind,
})

const response2 = grpcClient.invoke(methodName, grpcReq)
check(response2, {
'recall grpc stub - status is Internal': (r) => r && r.status === grpc.StatusInternal,
'recall grpc stub - check response message': (r) => r.error && r.error.message === `Can't find any stub for ${methodName}`,
})

}

export function teardown() {
scenarioTeardown()
grpcClient.close()
}
144 changes: 144 additions & 0 deletions backend/integrationTests/k6/v4/contentType/bidiStreaming.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import encoding from 'k6/encoding';
import http from 'k6/http';
import grpc from 'k6/net/grpc';
import { check, sleep } from 'k6';

import { serviceName } from './scenario.js'
import { httpHost, httpUri, httpOptions, grpcHost } from './scenario.js'
import { setup as scenarioSetup, teardown as scenarioTeardown } from './scenario.js'

const methodName = 'market_data.OTCMarketDataService/PricesBidi'

const grpcClient = new grpc.Client();
grpcClient.load(['definitions'], 'test_service.proto');

const protoFile = open('./definitions/test_service.proto');
const base64Proto = encoding.b64encode(protoFile);

export function setup() {
scenarioSetup()
}

export default function() {

const methodDescriptionData = {
id: "bidi-streaming-method-description",
description: "k6 testing",
service: serviceName,
methodName: methodName,
connectionType: "BIDI_STREAMING",
proxyUrl: null,
requestClass: "PricesRequest",
responseClass: "PricesResponse",
requestCodecs: base64Proto,
responseCodecs: base64Proto
}
const methodDescriptionRes = http.post(httpUri('/v4/grpcMethodDescription'), JSON.stringify(methodDescriptionData), httpOptions)
check(methodDescriptionRes, { 'create method description - status is OK': (r) => r.status === 200 })

const stubData = {
methodDescriptionId: "bidi-streaming-method-description",
scope: "countdown",
times: 2,
name: "bidirectional streaming v4 countdown stub",
response: {
"data": {
"code": "OK",
"instrument_id": "${req.instrument_id}",
"tracking_id": "${req.instrument_id_kind}"
},
"mode":"fill"
},
requestPredicates:{},
state: null,
seed: null,
persist: null,
labels: []
}
const stubRes = http.post(httpUri('/v4/grpcStub'), JSON.stringify(stubData), httpOptions)
check(stubRes, { 'create stub - status is OK': (r) => r.status === 200 })

grpcClient.connect(grpcHost, { plaintext: true })

const grpcReq = [
{ instrument_id: 'instrument_1', instrument_id_kind: 'ID_1' },
{ instrument_id: 'instrument_2', instrument_id_kind: 'ID_2' },
]

const stream = new grpc.Stream(grpcClient, methodName)
let inputStreamIndex = 0

stream.on('data', (data) => {
console.log('Stream#1 Value: ' + JSON.stringify(data));
check(data, {
'response stream element - validation is OK': (d) => d && inputStreamIndex < grpcReq.length &&
d.instrumentId === grpcReq[inputStreamIndex].instrument_id && d.trackingId === grpcReq[inputStreamIndex].instrument_id_kind,
});
inputStreamIndex += 1
})

stream.on('error', (err) => {
console.log('Stream#1 Error: ' + JSON.stringify(err));
check(err, { 'response stream element - should be empty': (e) => false })
})

stream.on('end', () => {
console.log('All done');
})

for (let i in grpcReq) {
stream.write(grpcReq[i])
sleep(0.1)
}

stream.end();


const stream2 = new grpc.Stream(grpcClient, methodName)

stream2.on('data', (data) => {
console.log('Stream#2 Value: ' + JSON.stringify(data));
check(data, { 'recall response stream element - should be empty': (d) => false })
})

stream2.on('error', (err) => {
console.log('Stream#2 Error: ' + JSON.stringify(err));
check(err, {
'recall response stream element - status is Internal': (e) => e && e.code == 13 &&
e.message === `Can't find any stub for ${methodName}`,
})
})

stream2.on('end', () => {
console.log('All done');
})

for (let i in grpcReq) {
stream2.write(grpcReq[i])
}

stream2.end();

}

export function teardown() {
scenarioTeardown()
grpcClient.close()
}

function makeStream(grpcClient, methodName, onData) {
const stream = new Stream(grpcClient, methodName)

stream.on('data', onData)

stream.on('error', (err) => {
console.log('Stream Error: ' + JSON.stringify(err));
})

stream.on('end', () => {
console.log('All done');
})

return stream
}

Loading

0 comments on commit 64a86c7

Please sign in to comment.