Skip to content

Commit

Permalink
feat: #13 Updating documentation and making test suite comprehensive
Browse files Browse the repository at this point in the history
Signed-off-by: Laurent Broudoux <[email protected]>
  • Loading branch information
lbroudoux committed Jan 4, 2024
1 parent dca6ba5 commit 5edda7b
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 9 deletions.
84 changes: 83 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ expect(testResult.testCaseResults[0].testStepResults[0].message).toContain("obje

The `TestResult` gives you access to all details regarding success of failure on different test cases.

A comprehensive NestJS demo application illustrating both usages is available here: [nest-order-service](https://github.com/microcks/api-lifecycle/tree/master/shift-left-demo/nest-order-service).

### Using authentication Secrets

It's a common need to authenticate to external systems like Http/Git repositories or external brokers. For that, the `MicrocksContainer` provides the `withSecret()` method to register authentication secrets at startup:

```ts
microcks.withSecret({
name: 'localstack secret',
username: 'test',
password: 'test'
})
.start();
```

You may reuse this secret using its name later on during a test like this:

```ts
const testRequest: TestRequest = {
serviceId: "Pastry orders API:0.1.0",
runnerType: TestRunnerType.ASYNC_API_SCHEMA,
testEndpoint: "sqs://us-east-1/pastry-orders?overrideUrl=http://localstack:4566",
secretName: "localstack secret",
timeout: 5000
}
```

### Advanced features with MicrocksContainersEnsemble

The `MicrocksContainer` referenced above supports essential features of Microcks provided by the main Microcks container. The list of supported features is the following:
Expand Down Expand Up @@ -139,6 +166,17 @@ await microcks.importAsMainArtifact(...);
.on("end", () => console.log("Stream closed"));
```

Please refer to our [MicrocksContainersEnsembleTest](https://github.com/microcks/microcks-testcontainers-node/blob/src/microcks-containers-ensemble.test.ts) for comprehensive example on how to use it.

#### Postman contract-testing

On this `ensemble` you may want to enable additional features such as Postman contract-testing:

```ts
ensemble.withPostman();
ensemble.start();
```

You can execute a `POSTMAN`` test using an ensemble that way:

```ts
Expand All @@ -157,4 +195,48 @@ expect(testResult.testCaseResults.length).toBe(3);
expect(testResult.testCaseResults[0].testStepResults[0].message).toBeUndefined();
```

Please refer to our [MicrocksContainersEnsembleTest](https://github.com/microcks/microcks-testcontainers-node/blob/src/microcks-containers-ensemble.test.ts) for comprehensive example on how to use it.
#### Asynchronous API support

Asynchronous API feature need to be explicitly enabled as well. In the case you want to use it for mocking purposes,
you'll have to specify additional connection details to the broker of your choice. See an example below with connection
to a Kafka broker:

```ts
ensemble.withAsyncFeature()
.withKafkaConnection({
bootstrapServers: "kafka:9092"
});
ensemble.start();
```

##### Using mock endpoints for your dependencies

Once started, the `ensemble.getAsyncMinionContainer()` provides methods for retrieving mock endpoint names for the different
supported protocols (WebSocket, Kafka, SQS and SNS).

```ts
const kafkaTopic = ensemble.getAsyncMinionContainer()
.getKafkaMockTopic("Pastry orders API", "0.1.0", "SUBSCRIBE pastry/orders");
```

##### Launching new contract-tests

Using contract-testing techniques on Asynchronous endpoints may require a different style of interacting with the Microcks
container. For example, you may need to:
1. Start the test making Microcks listen to the target async endpoint,
2. Activate your System Under Tests so that it produces an event,
3. Finalize the Microcks tests and actually ensure you received one or many well-formed events.

As the `MicrocksContainer` provides the `testEndpoint(request: TestRequest): Promise<TestResult>` method, this is actually easy like:

```ts
// Start the test, making Microcks listen the endpoint provided in testRequest
let testResultPromise: Promise<TestResult> = ensemble.getMicrocksContainer().testEndpoint(testRequest);

// Here below: activate your app to make it produce events on this endpoint.
// myapp.invokeBusinessMethodThatTriggerEvents();

// Now retrieve the final test result and assert.
let testResult = await testResultPromise;
expect(testResult.success).toBe(true);
```
12 changes: 6 additions & 6 deletions src/microcks-container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ export interface TestRequest {

export interface Secret {
name: string;
description: string;
username: string;
password: string;
token: string;
tokenHeader: string;
caCertPem: string;
description?: string;
username?: string;
password?: string;
token?: string;
tokenHeader?: string;
caCertPem?: string;
}

export interface SecretRef {
Expand Down
120 changes: 118 additions & 2 deletions src/microcks-containers-ensemble.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
*/
import * as path from "path";

import { CreateQueueCommand, ListQueuesCommand, ReceiveMessageCommand, SQSClient } from "@aws-sdk/client-sqs";
import { CreateQueueCommand, ListQueuesCommand, ReceiveMessageCommand, SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
import { GenericContainer, Network, Wait } from "testcontainers";
import { LocalstackContainer } from "@testcontainers/localstack";
import { MicrocksContainersEnsemble } from "./microcks-containers-ensemble";
import { TestRequest, TestRunnerType } from "./microcks-container";
import { TestRequest, TestResult, TestRunnerType } from "./microcks-container";
import { WebSocket } from "ws";

describe("MicrocksContainersEnsemble", () => {
Expand Down Expand Up @@ -301,6 +301,122 @@ describe("MicrocksContainersEnsemble", () => {
});
// }

// start and contract test async SQS {
it("should start, load artifacts and contract test mock async SQS", async () => {
const network = await new Network().start();

// Start ensemble, load artifacts and start other containers.
const localstack = await new LocalstackContainer("localstack/localstack:latest")
.withNetwork(network)
.withNetworkAliases("localstack")
.start();

// Create the Queue that has to be used by Microcks.
const client = new SQSClient({
region: "us-east-1",
credentials: {
accessKeyId: 'test',
secretAccessKey: 'test'
},
endpoint: {
url: new URL(localstack.getConnectionUri())
}
});
const createCommand = new CreateQueueCommand({
QueueName: 'pastry-orders'
})
await client.send(createCommand);

// Retrieve this queue URL
const listCommand = new ListQueuesCommand({
QueueNamePrefix: 'pastry-orders',
MaxResults: 1
});
let queueUrl;
try {
const listResponse = await client.send(listCommand);
queueUrl = listResponse.QueueUrls ? listResponse.QueueUrls[0] : "null";
} finally {
client.destroy();
}

const ensemble = await new MicrocksContainersEnsemble(network, "quay.io/microcks/microcks-uber:nightly")
.withMainArtifacts([path.resolve(resourcesDir, "pastry-orders-asyncapi.yml")])
.withSecret({
name: 'localstack secret',
username: 'test',
password: 'test'
})
.withAsyncFeature()
.start();

// Initialize messages, start the test and publish messages.
const badMessage = "{\"id\":\"abcd\",\"customerId\":\"efgh\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}";
const goodMessage = "{\"id\":\"abcd\",\"customerId\":\"efgh\",\"status\":\"CREATED\",\"productQuantities\":[{\"quantity\":2,\"pastryName\":\"Croissant\"},{\"quantity\":1,\"pastryName\":\"Millefeuille\"}]}";

var testRequest: TestRequest = {
serviceId: "Pastry orders API:0.1.0",
runnerType: TestRunnerType.ASYNC_API_SCHEMA,
testEndpoint: "sqs://us-east-1/pastry-orders?overrideUrl=http://localstack:4566",
secretName: "localstack secret",
timeout: 5000
}

// First test should fail with validation failure messages.
let testResultPromise: Promise<TestResult> = ensemble.getMicrocksContainer().testEndpoint(testRequest);

for (var i=0; i<5; i++) {
let sendCommand = new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: badMessage
});
client.send(sendCommand);
console.log('Sending bad message ' + i + ' on SQS queue');
delay(1000);
}

let testResult = await testResultPromise;

expect(testResult.success).toBe(false);
expect(testResult.testedEndpoint).toBe("sqs://us-east-1/pastry-orders?overrideUrl=http://localstack:4566");
expect(testResult.testCaseResults.length).toBeGreaterThan(0);
expect(testResult.testCaseResults[0].testStepResults[0].message).toContain("object has missing required properties");
testResult.testCaseResults.forEach(tcr => {
tcr.testStepResults.forEach(tsr => {
expect(tsr.message).toContain("object has missing required properties");
})
});

// Second test should be OK with no validation failure messages.
let testResultPromise2: Promise<TestResult> = ensemble.getMicrocksContainer().testEndpoint(testRequest);

for (var i=0; i<5; i++) {
let sendCommand = new SendMessageCommand({
QueueUrl: queueUrl,
MessageBody: goodMessage
});
client.send(sendCommand);
console.log('Sending good message ' + i + ' on SQS queue');
delay(1000);
}

let testResult2 = await testResultPromise2;

expect(testResult2.success).toBe(true);
expect(testResult2.testedEndpoint).toBe("sqs://us-east-1/pastry-orders?overrideUrl=http://localstack:4566");
expect(testResult2.testCaseResults.length).toBeGreaterThan(0);
testResult2.testCaseResults.forEach(tcr => {
tcr.testStepResults.forEach(tsr => {
expect(tsr.message).toBeUndefined();
})
});

// Now stop the ensemble, the containers and the network.
await ensemble.stop();
await localstack.stop();
await network.stop();
});
// }

function delay(ms: number) {
return new Promise( resolve => setTimeout(resolve, ms) );
Expand Down

0 comments on commit 5edda7b

Please sign in to comment.