Skip to content

Commit

Permalink
linter better tests
Browse files Browse the repository at this point in the history
Signed-off-by: d068544 <[email protected]>
  • Loading branch information
FrankEssenberger committed Feb 8, 2022
1 parent 8940540 commit da1cca8
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 108 deletions.
8 changes: 8 additions & 0 deletions samples/resilience-examples/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "http://json.schemastore.org/prettierrc",
"singleQuote": true,
"filepath": "*.ts",
"trailingComma": "none",
"arrowParens": "avoid",
"endOfLine": "lf"
}
12 changes: 12 additions & 0 deletions samples/resilience-examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SAP Cloud SDK for JS Resilience examples

## Description

This folder contains a few simples examples about resilience and the SDK.
The examples are using well established libraries and are tested for you.
They are meant as a blueprint to illustrate how resilience can be achieved, but there are many other ways out there which could be more fitting in your use case.

- In `src/circuit-breaker.ts` the opossum circuit breaker is wrapped around a request.
- In `src/retry.ts` the async-retry wrapper is used to retry failed requests.

There is always a `*.spec.ts` file next to the examples which shows the actual execution and that the libraries show the expected behaviour.
3 changes: 2 additions & 1 deletion samples/resilience-examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"lint": "eslint \"src/**/*.ts\" --fix",
"lint": "eslint \"src/**/*.ts\" && yarn prettier .",
"lint:fix": "eslint \"src/**/*.ts\" --fix && yarn prettier . -w",
"test": "jest"
},
"dependencies": {
Expand Down
86 changes: 49 additions & 37 deletions samples/resilience-examples/src/circuit-breaker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,41 +4,53 @@ import { createLogger } from '@sap-cloud-sdk/util';
import { getAllBusinessPartnerWithCircuitBreaker } from './circuit-breaker';
import { mockDestination, mockGetAllRequest } from './test-util';

describe('circuit-breaker',()=>{
const numberWorkingRequests = [1,2,3,4,5];

const logger = createLogger('circuit-breaker');

beforeAll(()=>{
mockDestination();
});

beforeEach(()=>{
mockGetAllRequest(numberWorkingRequests.length);
});

afterEach(()=>{
nock.cleanAll();
});

afterAll(()=>{
unmockAllTestDestinations();
});

it('does not open the cuircuit breaker for no failed requests.',async ()=>{
const spy = jest.spyOn(logger,'warn');
await Promise.all(numberWorkingRequests.map(()=>getAllBusinessPartnerWithCircuitBreaker(1)));
expect(spy).toHaveBeenCalledTimes(0);
});

it('opens the cuircuit after a few failed requests',async ()=>{
const spy = jest.spyOn(logger,'warn');

// The circuit breaker was configured to have 80% failure threshold. So 1 / 6 requests failing is enough to open the circuit
await Promise.all([...numberWorkingRequests,6].map(()=>getAllBusinessPartnerWithCircuitBreaker(1)));
expect(spy).toHaveBeenCalledWith('Request failed to many times. Circuit breaker is open.');

// Circuit breaker is now open and the request is not executed.
await expect(getAllBusinessPartnerWithCircuitBreaker(1)).resolves.not.toThrow();
});
describe('circuit-breaker', () => {
const numberWorkingRequests = [1, 2, 3, 4, 5];

const logger = createLogger('circuit-breaker');

beforeAll(() => {
mockDestination();
});

beforeEach(() => {
mockGetAllRequest(numberWorkingRequests.length);
});

afterEach(() => {
nock.cleanAll();
});

afterAll(() => {
unmockAllTestDestinations();
});

it('does not open the cuircuit breaker for no failed requests.', async () => {
const spy = jest.spyOn(logger, 'warn');
await Promise.all(
numberWorkingRequests.map(() =>
getAllBusinessPartnerWithCircuitBreaker(1)
)
);
expect(spy).toHaveBeenCalledTimes(0);
});

it('opens the cuircuit after a few failed requests', async () => {
const spy = jest.spyOn(logger, 'warn');

// The circuit breaker was configured to have 80% failure threshold. So 1 / 6 requests failing is enough to open the circuit
await Promise.all(
[...numberWorkingRequests, 6].map(() =>
getAllBusinessPartnerWithCircuitBreaker(1)
)
);
expect(spy).toHaveBeenCalledWith(
'Request failed to many times. Circuit breaker is open.'
);

// Circuit breaker is now open and the request is not executed.
await expect(
getAllBusinessPartnerWithCircuitBreaker(1)
).resolves.not.toThrow();
});
});
32 changes: 16 additions & 16 deletions samples/resilience-examples/src/circuit-breaker.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import { BusinessPartner, businessPartnerService } from '@sap/cloud-sdk-vdm-business-partner-service';
import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service';
import CircuitBreaker from 'opossum';
import { createLogger } from '@sap-cloud-sdk/util';
import { destinationName } from './test-util';
import { getAllBusinessPartner } from './test-util';

const logger = createLogger('circuit-breaker');

function getAllBusinessPartner(top: number): Promise<BusinessPartner[]>{
return businessPartnerService().businessPartnerApi.requestBuilder().getAll().top(top).execute({ destinationName });
}

const breakerOptions = {
timeout: 3000,
errorThresholdPercentage: 80,
resetTimeout: 30000
timeout: 3000,
errorThresholdPercentage: 80,
resetTimeout: 30000
};

// Create a new circuit breaker around the asyn method
const breaker = new CircuitBreaker(getAllBusinessPartner,breakerOptions);
// Create a new circuit breaker around the async method
const breaker = new CircuitBreaker(getAllBusinessPartner, breakerOptions);

// In the fallback you can put some logic to be executed if one system is not available.
breaker.fallback(()=>logger.warn('Request failed to many times. Circuit breaker is open.'));
// In the fallback you can put some logic to be executed if the breaker is open.
breaker.fallback(() =>
logger.warn('Request failed to many times. Circuit breaker is open.')
);

// Execute the asyn method passing arguments if needed
export function getAllBusinessPartnerWithCircuitBreaker(top: number): Promise<BusinessPartner[]>{
return breaker.fire(top);
// Execute the async method passing arguments if needed
export async function getAllBusinessPartnerWithCircuitBreaker(
top: number
): Promise<BusinessPartner[]> {
return breaker.fire(top);
}
89 changes: 55 additions & 34 deletions samples/resilience-examples/src/retry.spec.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,58 @@
import { unmockAllTestDestinations } from '@sap-cloud-sdk/test-util';
import nock from 'nock';
import { getAllBusinessPartners, getAllBusinessPartnerWithRetry } from './retry';
import { destinationUrl, mockDestination, mockGetAllRequest } from './test-util';

describe('retry',()=>{
beforeAll(()=>{
mockDestination();
});

beforeEach(()=>{
nock(destinationUrl)
.get(/.*/)
.times(2)
.reply(500);

mockGetAllRequest(1);
});

afterEach(()=>{
nock.cleanAll();
});

afterAll(()=>{
unmockAllTestDestinations();
});

it('fails without the retry.',async ()=>{
await expect(getAllBusinessPartners(1)()).rejects.toThrowError(`get request to ${destinationUrl}/sap/opu/odata/sap/API_BUSINESS_PARTNER failed!`);
});

it('resolves with retry',async ()=>{
// The mock will return after two failed attempts
const actual = await getAllBusinessPartnerWithRetry(1);
expect(actual.length).toBe(1);
});
import { createLogger } from '@sap-cloud-sdk/util';
import { getAllBusinessPartnerWithRetry } from './retry';
import {
destinationUrl,
getAllBusinessPartner,
mockDestination,
mockGetAllRequest
} from './test-util';

describe('retry', () => {
beforeAll(() => {
mockDestination();
});

beforeEach(() => {
nock(destinationUrl).get(/.*/).times(2).reply(500);

mockGetAllRequest(1);
});

afterEach(() => {
nock.cleanAll();
});

afterAll(() => {
unmockAllTestDestinations();
});

it('rejects without the retry.', async () => {
await expect(getAllBusinessPartner(1)).rejects.toThrowError(
`get request to ${destinationUrl}/sap/opu/odata/sap/API_BUSINESS_PARTNER failed!`
);
});

it('resolves with retry', async () => {
// The mock will return after two failed attempts
const actual = await getAllBusinessPartnerWithRetry(1);
expect(actual.length).toBe(1);
});

it('does not retry on 403 error', async () => {
nock.cleanAll();
nock(destinationUrl).get(/.*/).times(1).reply(401);

const logger = createLogger('retry');
const spy = jest.spyOn(logger, 'warn');
await expect(getAllBusinessPartnerWithRetry(1)).rejects.toThrowError(
`get request to ${destinationUrl}/sap/opu/odata/sap/API_BUSINESS_PARTNER failed!`
);

expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledWith(
'Request failed with status 401 - no retry necessary.'
);
});
});
33 changes: 24 additions & 9 deletions samples/resilience-examples/src/retry.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import { BusinessPartner, businessPartnerService } from '@sap/cloud-sdk-vdm-business-partner-service';
import { BusinessPartner } from '@sap/cloud-sdk-vdm-business-partner-service';
import retry from 'async-retry';
import { destinationName } from './test-util';
import { createLogger } from '@sap-cloud-sdk/util';
import { getAllBusinessPartner } from './test-util';

export function getAllBusinessPartners(top: number): () => Promise<BusinessPartner[]>{
return ()=> businessPartnerService().businessPartnerApi.requestBuilder().getAll().top(top).execute({ destinationName });
}
const logger = createLogger('retry');

const options = {
retries : 2,
minTimeout: 500
retries: 2,
minTimeout: 500
};

export function getAllBusinessPartnerWithRetry(top: number): Promise<BusinessPartner[]>{
return retry(getAllBusinessPartners(top),options);
// Create a wrapper passing arguments
export async function getAllBusinessPartnerWithRetry(
top: number
): Promise<BusinessPartner[]> {
// Wrap the retry block around the async function.
return retry(async bail => {
try {
const bps = await getAllBusinessPartner(top);
return bps;
} catch (error) {
// Use the bail() method to stop the retries for some cases
if (error.cause.response.status === 401) {
logger.warn('Request failed with status 401 - no retry necessary.');
bail(error);
}
throw error;
}
}, options);
}
33 changes: 22 additions & 11 deletions samples/resilience-examples/src/test-util.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { setTestDestination } from '@sap-cloud-sdk/test-util';
import nock from 'nock';
import {
BusinessPartner,
businessPartnerService
} from '@sap/cloud-sdk-vdm-business-partner-service';

export const destinationName = 'MyDestination';
export const destinationUrl ='http://some.url.com';
export const destinationUrl = 'http://some.url.com';

export function mockDestination(): void{
setTestDestination({ name: destinationName,url:destinationUrl });
export function mockDestination(): void {
setTestDestination({ name: destinationName, url: destinationUrl });
}

export const sampleResponse = {
d: {
results: [{ BusinessPartner:1234 }]
}
d: {
results: [{ BusinessPartner: 1234 }]
}
};

export function mockGetAllRequest(times: number): void{
nock(destinationUrl)
.get(/.*/)
.times(times)
.reply(200,sampleResponse);
export function mockGetAllRequest(times: number): void {
nock(destinationUrl).get(/.*/).times(times).reply(200, sampleResponse);
}

export async function getAllBusinessPartner(
top: number
): Promise<BusinessPartner[]> {
return businessPartnerService()
.businessPartnerApi.requestBuilder()
.getAll()
.top(top)
.execute({ destinationName });
}
1 change: 1 addition & 0 deletions samples/resilience-examples/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"lib": ["ES2017"],
"esModuleInterop": true,
"target": "es2017",
"sourceMap": true,
Expand Down

0 comments on commit da1cca8

Please sign in to comment.