简体   繁体   English

如何测试AWS Lambda

[英]How to test AWS lambdas

So, I have some AWS lambda code that connects to a Redis Cluster instance (using ioredis) to store data. 因此,我有一些AWS lambda代码可以连接到Redis群集实例(使用ioredis)来存储数据。 We want to instantiate the Cluster in the lambda container for reuse, as we intend to have this lambda hit frequently enough that we would gain performance benefits from container reuse. 我们希望在lambda容器中实例化集群以进行重用,因为我们打算使此lambda受到足够的攻击,以便我们从容器重用中获得性能收益。

We have written a wrapper class (called RedisCluster) which extends the Redis Cluster to provide additional checks and functionality, as we are using this in a variety of different lambdas. 我们编写了一个包装器类(称为RedisCluster),该类扩展了Redis集群以提供其他检查和功能,因为我们在各种不同的lambda中使用了该类。

In the past, I have been able to stub out Redis Cluster instances when testing, but when I instantiate it in the container it appears that the Cluster is spun up when the lambda source code is loaded (ie before test execution). 过去,我能够在测试时将Redis Cluster实例存根,但是当我在容器中实例化Redis Cluster实例时,似乎在加载Lambda源代码时(即在测试执行之前),群集被启动。 When my tests are run, I get an error indicating that the Cluster is unable to connect to node instances. 运行测试时,我收到一条错误消息,指示集群无法连接到节点实例。

The lambda λ

let redisCluster = new RedisCluster([{ host: process.env.HOST, port: process.env.PORT }]);

function isKeyInCache(cacheKey) {
    logger.info(`Searching for key::${cacheKey}`);
    return redisCluster.get(cacheKey);
}

exports.handler = baseHandler((e, ctx, cb) => {
    ctx.callbackWaitsForEmptyEventLoop = false;
    const cacheKey = `${key}`;
    isKeyInCache(cacheKey).then((response) => {
        if (response) {
            logger.info('Key is registered');
            redisCluster.removeKey(cacheKey).then(() => {
                const result = { status: 'Registered' };
                cb(null, result);
            }).catch((err) => {
                logger.error(err);
                cb(err, 'Error');
            });
        } else {
            const result = { status: 'NotFound' };
            cb(null, result);
        }
    });

    redisCluster.on('error', () => {
        cb('An error has occurred with the redis cluster');
    });

Here is the wrapper class 这是包装器类

class RedisCluster extends Redis.Cluster {
    constructor(opts) {
        super(opts);
    }

    removeKey(cacheKey) {
        return new Promise((resolve, reject) => {
            super.del(cacheKey, (err, reply) => {
                if (err) {
                    logger.error(`Failed to remove key::${cacheKey} Error response: ${err}`);
                    reject(err);
                } else {
                    logger.info(`Successfully removed key::${cacheKey} response: ${reply}`);
                    resolve();
                }
            });
        });
    }

    quitRedisCluster() {
        return new Promise((resolve, reject) => {
            if (this) {
                super.quit(() => {
                    logger.info('Quitting redis cluster connection...');
                    resolve();
                }).catch((err) => {
                    logger.error('Error closing cluster');
                    reject(err);
                });
            } else {
                logger.error('Cluster not defined');
                reject();
            }
        });
    }
}
module.exports = RedisCluster;

I am unable to properly inject any dependencies (this being a lambda) and stubbing the Redis Cluster does not seem to work as it is instantiated when the source code is loaded. 我无法正确注入任何依赖项(这是lambda),并且对Redis群集进行存根处理似乎不起作用,因为在加载源代码时已实例化它。 However, I am able to replace the Redis Cluster before testing by adding an exported function to do so. 但是,我可以通过添加导出功能来替换Redis Cluster,然后再进行测试。 This is ugly, and the method is only used for testing... so I imagine there has to be a better way to do this. 这很丑陋,该方法用于测试...所以我想必须有一种更好的方法来执行此操作。

Here is the method I've added to the lambda to mock the Cluster. 这是我添加到lambda中以模拟集群的方法。 Unfortunately the initial Cluster still gets spun up when the lambda code is loaded, so I get connection errors littering my test output, although this does work when I inject a spy or stub. 不幸的是,当装入lambda代码时,初始Cluster仍然会启动,因此我收到连接错误,乱扔了我的测试输出,尽管在注入间谍程序或存根时确实可以工作。 I do not like this because it produces code smell and is a method added ONLY to satisfy tests. 我不喜欢这样做,因为它会产生代码异味,并且只是为了满足测试而添加的一种方法。

exports.injectCluster = (injectedDependency) => {
    redisCluster.disconnect();
    redisCluster = injectedDependency;
};

My tests look something like this. 我的测试看起来像这样。

import Promise from 'bluebird';
import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import Context from 'mock-ctx';
import sinon from 'sinon';
import { handler, injectCluster } from '../src';

chai.use(chaiAsPromised);

let redisClusterConstructor;
let removeKey;
let on;
let disconnect;
let get;
const ctx = new Context();

describe('lambda', () => {
    beforeEach(() => {
        removeKey = sinon.spy(() => Promise.resolve({}));
        on = sinon.spy((e, cb) => {});
        disconnect = sinon.spy(() => {});

        redisClusterConstructor = {
            removeKey,
            on,
            disconnect
         };
    });

    it('should get key in redis if key exist', () => {
        get = sinon.spy((k) => Promise.resolve('true'));
        redisClusterConstructor['get'] = get;
        injectCluster(redisClusterConstructor);
        const promise = Promise.fromCallback(cb => handler(e, ctx, cb));
        return chai.expect(promise).to.be.fulfilled.then((response) => {
            chai.assert.isTrue(get.calledOnce);
            chai.assert.isTrue(removeKey.calledOnce);
            chai.expect(response).to.deep.equal({ status: 'Registered' });
        });
    });
});

Things I have tried: 我尝试过的事情:

1: Stubbing the 'class' using sinon 1:使用sinon对“类”进行存根

Won't work because javascript objects aren't really classes. 无法正常工作,因为javascript对象不是真正的类。 I cannot seem to stub out the constructor, only the methods, so the Cluster still gets initially spun up in the constructor. 我似乎无法仅对方法进行构造,因此群集仍然最初会在结构中启动。

2: Rewiring the import 2:重新导入

Won't seem to work because of the order in which things are executed in the tests. 由于测试中执行事情的顺序似乎不起作用。 When the lambda code is loaded the RedisCluster immediately spins up. 加载lambda代码后,RedisCluster立即旋转。 So, by the time the test actually runs the Cluster already exists and will not use the rewired import. 因此,到测试实际运行时,群集已经存在,将不再使用重新导入的导入。

3: 'Dependency injection' 3:“依赖注入”

Works, but is ugly and likely won't pass the PR process... 可行,但很难看,可能无法通过PR程序...

4: Rewriting the wrapper class to wait to connect until the first command is executed 4:重写包装器类以等待连接,直到执行第一个命令

This is what I am trying now, I have not finished the code to know if it will work or not. 这就是我现在正在尝试的内容,我还没有完成代码以了解它是否可以工作。

Am I on the right track?... In Java this is simple as pie, but I cannot for the life of me figure out what to do in node to cleanly mock this dependency. 我在正确的轨道上吗?...在​​Java中,这很简单,但是我一生都无法弄清楚在节点中做什么以完全模拟这种依赖关系。

I found a solution to my issue, and although it's not ideal it does address the shortcomings in the mock dependency injection solution I had come up with earlier. 我找到了解决问题的方法,尽管它不是理想的方法,但确实可以解决我先前提出的模拟依赖注入解决方案中的缺点。

I simply changed my instance of my wrapper RedisCluster class to be instantiated with process.env.opts instead of { host: process.env.HOST, port: process.env.PORT } , where opts is simply the map used above. 我只是将包装RedisCluster类的实例更改为使用process.env.opts而不是{ host: process.env.HOST, port: process.env.PORT }实例化,其中opts只是上面使用的映射。

Then in my test file I included the statement process.env.opts = { mock: "stuff" } before the lambda source code was being loaded to avoid the Redis Cluster attempting to connect to an instance of a Cluster that didn't exist. 然后,在我的测试文件中,在加载lambda源代码之前,我加入了语句process.env.opts = { mock: "stuff" } ,以避免Redis Cluster尝试连接到不存在的Cluster实例。 Then I stubbed out the methods to the wrapper class using sinon as usual. 然后,像往常一样使用sinon将方法存入包装类。

I hope this helps someone. 我希望这可以帮助别人。 It won't pass a linter as is as eslint requires import statements to be at the top. 它不会像eslint那样通过linter,因为eslint要求import语句位于顶部。 So I moved that statement to another js file and included it before I included the source file.. 因此,在将源文件包含在内之前,我将该语句移至另一个js文件并包含了它。

A bit hacky, but it works. 有点hacky,但是可以用。 If there is a better way someone please chime in. 如果有更好的方法,请有人发出声音。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM