模拟 Javascript AWS.RDS.Signer
Mock Javascript AWS.RDS.Signer
我有用于通过 IAM 身份验证连接到 AWS Rds 代理的连接 class。该过程的一部分是创建令牌。我有创建令牌的功能,但现在我很难模拟和测试它。
这里是 class 与 setToken
方法的连接:
class Connection {
constructor(username, endpoint, database) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
}
setToken () {
let signer = new AWS.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
这里我试图模拟 AWS.RDS.Signer.getAuthToken()
的 return 值
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb');
conn.setToken();
console.log(conn.token);
});
我希望看到“mock-token”作为 conn.token
的值,但我得到的是:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
如何获得 AWS.RDS.Signer.getAuthToken()
到 return 模拟令牌?
尝试@ggordon
的解决方案后进行编辑
我试图通过将 AWS
注入构造函数来使其工作,但似乎仍然存在相同的问题。我认为我的部分问题是 AWS.RDS.Signer 不支持承诺,但我不完全确定。
这是我的新代码:
生成令牌的Token
class。
从 'aws-sdk';
导入 AWS
class Token {
constructor(awsInstance) {
this.awsInstance = awsInstance || AWS;
}
getToken () {
const endpoint = 'aurora-proxy.proxy.rds.amazonaws.com';
const signer = new this.awsInstance.RDS.Signer({
region: 'my-region',
hostname: endpoint,
port: 3306,
username: 'myUser'
});
const token = signer.getAuthToken({
username: 'svcLambda'
});
console.log ("IAM Token obtained\n");
return token
}
}
module.exports = { Token };
测试:
test('Should test getToken from Token', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let tokenObject = new tokens.Token(AWS);
const token = tokenObject.getToken();
console.log(token);
expect(token).toStrictEqual('mock-token');
});
令牌 class 本身有效——它创建令牌,令牌可用于成功连接到 RDS。但是,单元测试失败,实际标记 returned(来自 console.log)是这样的:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
这里还有@GSSWain
要求的package.json
{
"name": "mylambda",
"version": "0.0.1",
"description": "My description.",
"repository": {
"type": "git",
"url": ""
},
"scripts": {
"lint": "eslint src/**/*.js __tests__/**/*.js",
"prettier": "prettier --write src/**/*.js __tests__/**/*.js",
"prettier:ci": "prettier --list-different src/**/*.js __tests__/**/*.js",
"test": "cross-env NODE_ENV=test jest",
"test:coverage": "cross-env CI=true jest --coverage --watchAll=false -u --reporter=default --reporters=jest-junit",
"build": "npm run build:dev",
"build:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js"
},
"dependencies": {
"mysql2": "^2.2.5"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"aws-sdk": "^2.552.0",
"aws-sdk-mock": "^5.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"cross-env": "^6.0.3",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-jest": "^22.19.0",
"jest": "^24.9.0",
"jest-junit": "^10.0.0",
"prettier": "^1.18.2",
"sinon": "^9.0.3"
},
"jest": {
"verbose": true,
"transform": {
"^.+\.js$": "babel-jest"
},
"globals": {
"NODE_ENV": "test"
},
"moduleFileExtensions": [
"js"
],
"moduleDirectories": [
"node_modules",
"src"
],
"coverageThreshold": {
"global": {
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}
},
"jest-junit": {
"outputName": "junit_jest.xml"
}
}
问题
您的测试范围中的 AWS
instance/object 与您的 setToken
方法中使用的 AWS
instance/object 不同。
aws-sdk-mock
模拟这个实例
Due to transpiling, code written in TypeScript or ES6 may not correctly mock because the aws-sdk object created within aws-sdk-mock will not be equal to the object created within the code to test.
另外 require
将 return 一个新实例。
本质上,您是在模拟测试中的一个实例,而您的实际代码正在使用另一个未被模拟的实例。
可能的解决方案
解决方案 1
您可以修改代码以允许您有选择地注入所需的 AWS
实例以使用 eg
import AWS from 'aws-sdk';
class Connection {
constructor(username, endpoint, database,awsInstance) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
//if the awsInstance is null or not provided use the default
this.awsInstance = awsInstance || AWS;
}
setToken () {
let signer = new this.awsInstance.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
您的代码不需要任何修改,但是现在您可以选择在测试中使用
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
let actualToken = (await conn.token.promise());
console.log(conn.token);
console.log(actualToken);
});
这只是基于构造函数的注入,您可以通过在 setToken
方法中执行类似的操作来注入它。
您还会注意到,在 aws-sdk-mock
提供的示例和上面的示例中,我们从 promise
对象 returned 中提取了结果。这是因为 mock implementation returns a promise object despite the fact that the aws-sdk
especially for the AWS.RDS.Signer.getAuthToken
支持同步操作。 这是基于您正在使用的库的约束。
解决方案 2
如果您对同步调用感兴趣,您可能需要考虑另一个模拟库,基于此处共享的示例可以更好地模仿您的 code/flow。另一种选择是考虑 asynchronous/promise 重写您的实现。我把这个决定留给你。
一个简单的替代方案可以是:
test('Test Connection setToken', async () => {
AWS.RDS.Signer = function MockSigner() {
return {
getAuthToken: function MockGetAuthToken(){ return 'mock-token'; }
};
};
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
console.log(conn.token);
});
其他参考资料
我包含了一段用于模拟从 https://github.com/dwyl/aws-sdk-mock/blob/master/index.js#L118 检索到的 aws-sdk-mock
函数的方法。您会看到它创建并 returns 一个 request
function mockServiceMethod(service, client, method, replace) {
services[service].methodMocks[method].stub = sinon.stub(client, method).callsFake(function() {
const args = Array.prototype.slice.call(arguments);
let userArgs, userCallback;
if (typeof args[(args.length || 1) - 1] === 'function') {
userArgs = args.slice(0, -1);
userCallback = args[(args.length || 1) - 1];
} else {
userArgs = args;
}
const havePromises = typeof AWS.Promise === 'function';
let promise, resolve, reject, storedResult;
const tryResolveFromStored = function() {
if (storedResult && promise) {
if (typeof storedResult.then === 'function') {
storedResult.then(resolve, reject)
} else if (storedResult.reject) {
reject(storedResult.reject);
} else {
resolve(storedResult.resolve);
}
}
};
const callback = function(err, data) {
if (!storedResult) {
if (err) {
storedResult = {reject: err};
} else {
storedResult = {resolve: data};
}
}
if (userCallback) {
userCallback(err, data);
}
tryResolveFromStored();
};
const request = {
promise: havePromises ? function() {
if (!promise) {
promise = new AWS.Promise(function (resolve_, reject_) {
resolve = resolve_;
reject = reject_;
});
}
tryResolveFromStored();
return promise;
} : undefined,
createReadStream: function() {
if (replace instanceof Readable) {
return replace;
} else {
const stream = new Readable();
stream._read = function(size) {
if (typeof replace === 'string' || Buffer.isBuffer(replace)) {
this.push(replace);
}
this.push(null);
};
return stream;
}
},
on: function(eventName, callback) {
},
send: function(callback) {
}
};
// different locations for the paramValidation property
const config = (client.config || client.options || _AWS.config);
if (config.paramValidation) {
try {
// different strategies to find method, depending on wether the service is nested/unnested
const inputRules =
((client.api && client.api.operations[method]) || client[method] || {}).input;
if (inputRules) {
const params = userArgs[(userArgs.length || 1) - 1];
new _AWS.ParamValidator((client.config || _AWS.config).paramValidation).validate(inputRules, params);
}
} catch (e) {
callback(e, null);
return request;
}
}
// If the value of 'replace' is a function we call it with the arguments.
if (typeof replace === 'function') {
const result = replace.apply(replace, userArgs.concat([callback]));
if (storedResult === undefined && result != null &&
typeof result.then === 'function') {
storedResult = result
}
}
// Else we call the callback with the value of 'replace'.
else {
callback(null, replace);
}
return request;
});
}
我有用于通过 IAM 身份验证连接到 AWS Rds 代理的连接 class。该过程的一部分是创建令牌。我有创建令牌的功能,但现在我很难模拟和测试它。
这里是 class 与 setToken
方法的连接:
class Connection {
constructor(username, endpoint, database) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
}
setToken () {
let signer = new AWS.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
这里我试图模拟 AWS.RDS.Signer.getAuthToken()
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb');
conn.setToken();
console.log(conn.token);
});
我希望看到“mock-token”作为 conn.token
的值,但我得到的是:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
如何获得 AWS.RDS.Signer.getAuthToken()
到 return 模拟令牌?
尝试@ggordon
的解决方案后进行编辑我试图通过将 AWS
注入构造函数来使其工作,但似乎仍然存在相同的问题。我认为我的部分问题是 AWS.RDS.Signer 不支持承诺,但我不完全确定。
这是我的新代码:
生成令牌的Token
class。
从 'aws-sdk';
class Token {
constructor(awsInstance) {
this.awsInstance = awsInstance || AWS;
}
getToken () {
const endpoint = 'aurora-proxy.proxy.rds.amazonaws.com';
const signer = new this.awsInstance.RDS.Signer({
region: 'my-region',
hostname: endpoint,
port: 3306,
username: 'myUser'
});
const token = signer.getAuthToken({
username: 'svcLambda'
});
console.log ("IAM Token obtained\n");
return token
}
}
module.exports = { Token };
测试:
test('Should test getToken from Token', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let tokenObject = new tokens.Token(AWS);
const token = tokenObject.getToken();
console.log(token);
expect(token).toStrictEqual('mock-token');
});
令牌 class 本身有效——它创建令牌,令牌可用于成功连接到 RDS。但是,单元测试失败,实际标记 returned(来自 console.log)是这样的:
{
promise: [Function],
createReadStream: [Function: createReadStream],
on: [Function: on],
send: [Function: send]
}
这里还有@GSSWain
要求的package.json
{
"name": "mylambda",
"version": "0.0.1",
"description": "My description.",
"repository": {
"type": "git",
"url": ""
},
"scripts": {
"lint": "eslint src/**/*.js __tests__/**/*.js",
"prettier": "prettier --write src/**/*.js __tests__/**/*.js",
"prettier:ci": "prettier --list-different src/**/*.js __tests__/**/*.js",
"test": "cross-env NODE_ENV=test jest",
"test:coverage": "cross-env CI=true jest --coverage --watchAll=false -u --reporter=default --reporters=jest-junit",
"build": "npm run build:dev",
"build:dev": "cross-env NODE_ENV=development webpack --config webpack.config.js"
},
"dependencies": {
"mysql2": "^2.2.5"
},
"devDependencies": {
"@babel/core": "^7.6.4",
"@babel/preset-env": "^7.6.3",
"aws-sdk": "^2.552.0",
"aws-sdk-mock": "^5.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "^8.0.6",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"cross-env": "^6.0.3",
"eslint": "^6.5.1",
"eslint-config-prettier": "^6.4.0",
"eslint-plugin-jest": "^22.19.0",
"jest": "^24.9.0",
"jest-junit": "^10.0.0",
"prettier": "^1.18.2",
"sinon": "^9.0.3"
},
"jest": {
"verbose": true,
"transform": {
"^.+\.js$": "babel-jest"
},
"globals": {
"NODE_ENV": "test"
},
"moduleFileExtensions": [
"js"
],
"moduleDirectories": [
"node_modules",
"src"
],
"coverageThreshold": {
"global": {
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 100
}
}
},
"jest-junit": {
"outputName": "junit_jest.xml"
}
}
问题
您的测试范围中的 AWS
instance/object 与您的 setToken
方法中使用的 AWS
instance/object 不同。
aws-sdk-mock
模拟这个实例
Due to transpiling, code written in TypeScript or ES6 may not correctly mock because the aws-sdk object created within aws-sdk-mock will not be equal to the object created within the code to test.
另外 require
将 return 一个新实例。
本质上,您是在模拟测试中的一个实例,而您的实际代码正在使用另一个未被模拟的实例。
可能的解决方案
解决方案 1
您可以修改代码以允许您有选择地注入所需的 AWS
实例以使用 eg
import AWS from 'aws-sdk';
class Connection {
constructor(username, endpoint, database,awsInstance) {
this.username = username;
this.endpoint = endpoint;
this.database = database;
//if the awsInstance is null or not provided use the default
this.awsInstance = awsInstance || AWS;
}
setToken () {
let signer = new this.awsInstance.RDS.Signer({
region: 'us-east-1', // example: us-east-2
hostname: this.endpoint,
port: 3306,
username: this.username
});
this.token = signer.getAuthToken({
username: this.username
});
}
}
您的代码不需要任何修改,但是现在您可以选择在测试中使用
test('Test Connection setToken', async () => {
AWSMock.setSDKInstance(AWS);
AWSMock.mock('RDS.Signer', 'getAuthToken', 'mock-token');
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
let actualToken = (await conn.token.promise());
console.log(conn.token);
console.log(actualToken);
});
这只是基于构造函数的注入,您可以通过在 setToken
方法中执行类似的操作来注入它。
您还会注意到,在 aws-sdk-mock
提供的示例和上面的示例中,我们从 promise
对象 returned 中提取了结果。这是因为 mock implementation returns a promise object despite the fact that the aws-sdk
especially for the AWS.RDS.Signer.getAuthToken
支持同步操作。 这是基于您正在使用的库的约束。
解决方案 2
如果您对同步调用感兴趣,您可能需要考虑另一个模拟库,基于此处共享的示例可以更好地模仿您的 code/flow。另一种选择是考虑 asynchronous/promise 重写您的实现。我把这个决定留给你。
一个简单的替代方案可以是:
test('Test Connection setToken', async () => {
AWS.RDS.Signer = function MockSigner() {
return {
getAuthToken: function MockGetAuthToken(){ return 'mock-token'; }
};
};
let conn = new connections.Connection(
'testUser',
'testEndpoint',
'testDb',
AWS //pass mock instance
);
conn.setToken();
console.log(conn.token);
});
其他参考资料
我包含了一段用于模拟从 https://github.com/dwyl/aws-sdk-mock/blob/master/index.js#L118 检索到的 aws-sdk-mock
函数的方法。您会看到它创建并 returns 一个 request
function mockServiceMethod(service, client, method, replace) {
services[service].methodMocks[method].stub = sinon.stub(client, method).callsFake(function() {
const args = Array.prototype.slice.call(arguments);
let userArgs, userCallback;
if (typeof args[(args.length || 1) - 1] === 'function') {
userArgs = args.slice(0, -1);
userCallback = args[(args.length || 1) - 1];
} else {
userArgs = args;
}
const havePromises = typeof AWS.Promise === 'function';
let promise, resolve, reject, storedResult;
const tryResolveFromStored = function() {
if (storedResult && promise) {
if (typeof storedResult.then === 'function') {
storedResult.then(resolve, reject)
} else if (storedResult.reject) {
reject(storedResult.reject);
} else {
resolve(storedResult.resolve);
}
}
};
const callback = function(err, data) {
if (!storedResult) {
if (err) {
storedResult = {reject: err};
} else {
storedResult = {resolve: data};
}
}
if (userCallback) {
userCallback(err, data);
}
tryResolveFromStored();
};
const request = {
promise: havePromises ? function() {
if (!promise) {
promise = new AWS.Promise(function (resolve_, reject_) {
resolve = resolve_;
reject = reject_;
});
}
tryResolveFromStored();
return promise;
} : undefined,
createReadStream: function() {
if (replace instanceof Readable) {
return replace;
} else {
const stream = new Readable();
stream._read = function(size) {
if (typeof replace === 'string' || Buffer.isBuffer(replace)) {
this.push(replace);
}
this.push(null);
};
return stream;
}
},
on: function(eventName, callback) {
},
send: function(callback) {
}
};
// different locations for the paramValidation property
const config = (client.config || client.options || _AWS.config);
if (config.paramValidation) {
try {
// different strategies to find method, depending on wether the service is nested/unnested
const inputRules =
((client.api && client.api.operations[method]) || client[method] || {}).input;
if (inputRules) {
const params = userArgs[(userArgs.length || 1) - 1];
new _AWS.ParamValidator((client.config || _AWS.config).paramValidation).validate(inputRules, params);
}
} catch (e) {
callback(e, null);
return request;
}
}
// If the value of 'replace' is a function we call it with the arguments.
if (typeof replace === 'function') {
const result = replace.apply(replace, userArgs.concat([callback]));
if (storedResult === undefined && result != null &&
typeof result.then === 'function') {
storedResult = result
}
}
// Else we call the callback with the value of 'replace'.
else {
callback(null, replace);
}
return request;
});
}