首页 > 解决方案 > 使用带有 promises 和 mocha 的 Sinon.fakeServer

问题描述

我的问题如下:我想测试一种将大量数据上传到 AWS S3 存储桶的方法。问题是:我不想在每次测试时都上传数据,也不想关心环境中的凭据。所以我想设置 Sinon 的 fake-server 模块来模拟上传并返回与 S3 相同的结果。可悲的是,似乎很难找到使用 async/await 的代码的工作示例。

我的测试如下所示:

import {skip, test, suite} from "mocha-typescript";
import Chai from "chai";
import {S3Uploader} from "./s3-uploader.class";
import Sinon from "sinon";

@suite
class S3UploaderTest {

    public server : Sinon.SinonFakeServer | undefined;

    before() {
        this.server = Sinon.fakeServer.create();
    }

    after() {
        if (this.server != null) this.server.restore();
    }

    @test
    async "should upload a file to s3 correctly"(){

        let spy = Sinon.spy();

        const uploader : S3Uploader = new S3Uploader();
        const upload = await uploader.send("HalloWelt").toBucket("onetimeupload.test").toFolder("test/hw.txt").upload();

        Chai.expect(upload).to.be.a("object");
    }

}

在 uploader.upload() 方法中,我从回调中解决了一个承诺。那么如何模拟上传过程呢?

编辑:这是 s3-uploader 的代码:

import AWS from "aws-sdk";

export class S3Uploader {
    private s3 = new AWS.S3({ accessKeyId : process.env.ACCESS_KEY_ID,  secretAccessKey : process.env.SECRET_ACCESS_KEY });
    private params = {
        Body: null || Object,
        Bucket: "",
        Key: ""
    };

    public send(stream : any) {
        this.params.Body = stream;
        return this;
    }

    public toBucket(bucket : string) {
        this.params.Bucket = bucket;
        return this;
    }

    public toFolder(path : string) {
        this.params.Key = path;
        return this;
    }

    public upload() {
        return new Promise((resolve, reject) => {

            if (process.env.ACCESS_KEY_ID == null || process.env.SECRET_ACCESS_KEY == null) {
                return reject("ERR_NO_AWS_CREDENTIALS");
            }

            this.s3.upload(this.params, (error : any, data : any) => {
                return error ? reject(error) : resolve(data);
            });
        });
    }
}

标签: node.jstypescriptmocha.jssinon

解决方案


AWS.S3Sinon 假服务器是您可以用来开发一个本身发出请求的客户端的东西,而不是像您正在做的那样围绕现有客户端的包装器。在这种情况下,您最好只是对行为进行存根,AWS.S3而不是测试它发出的实际请求。这样您就可以避免测试AWS.S3.

由于您使用的是 TypeScript 并且已经制作了 s3 客户端private,因此您需要进行一些更改以将其公开给您的测试。否则,您将无法在没有 TS 编译器抱怨的情况下对其方法进行存根。params出于类似的原因,您也将无法使用该对象编写断言。

由于我不经常使用 TS,因此我不太熟悉它的常见依赖注入技术,但您可以做的一件事是向您的S3Uploader类添加可选的构造函数参数,该参数可以覆盖默认值s3arguments属性,如下所示:

constructor(s3, params) {
    if (s3) this.s3 = s3;
    if (params) this.params = params;
}

之后,您可以创建一个存根实例并将其传递给您的测试实例,如下所示:

const s3 = sinon.createStubInstance(AWS.S3);
const params = { foo: 'bar' };
const uploader = new S3Uploader(s3, params);

一旦你有了存根实例,你就可以编写断言来确保upload方法被调用的方式是你想要的:

sinon.assert.calledOnce(s3.upload);
sinon.assert.calledWith(s3.upload, sinon.match.same(params), sinon.match.func);

upload您还可以使用sinon stub api影响该方法的行为。例如,让它像这样失败:

s3.upload.callsArgWith(1, null);

或者让它像这样成功:

const data = { whatever: 'data', you: 'want' };
s3.upload.callsArgWith(1, null, data);

您可能需要对每种情况进行完全独立的测试,使用实例before挂钩来避免重复常见的设置内容。成功测试将包括简单地await实现承诺并检查其结果是否是数据。失败测试将涉及try/catch确保 promise 因正确错误而被拒绝。

此外,由于您似乎在这里进行实际的单元测试,我建议单独测试每个 S3Uploader 方法,而不是在一次大测试中调用它们。这大大减少了您需要涵盖的可能案例的数量,使您的测试更加直接。像这样的东西:

@suite
class S3UploaderTest {
    params: any; // Not sure the best way to type this.
    s3: any; // Same. Sorry, not too experienced with TS.
    uploader: S3Uploader | undefined;

    before() {
        this.params = {};
        this.s3 = sinon.createStubInstance(AWS.S3);
        this.uploader = new S3Uploader(this.s3, this.params);
    }

    @test
    "send should set Body param and return instance"() {
        const stream = "HalloWelt";
        const result = this.uploader.send(stream);
        Chai.expect(this.params.Body).to.equal(stream);
        Chai.expect(result).to.equal(this.uploader);
    }

    @test
    "toBucket should set Bucket param and return instance"() {
        const bucket = "onetimeupload.test"
        const result = this.uploader.toBucket(bucket);
        Chai.expect(this.params.Bucket).to.equal(bucket);
        Chai.expect(result).to.equal(this.uploader);
    }

    @test
    "toFolder should set Key param and return instance"() {
        const path = "onetimeupload.test"
        const result = this.uploader.toFolder(path);
        Chai.expect(this.params.Key).to.equal(path);
        Chai.expect(result).to.equal(this.uploader);
    }

    @test
    "upload should attempt upload to s3"() {
        this.uploader.upload();
        sinon.assert.calledOnce(this.s3.upload);
        sinon.assert.calledWith(
            this.s3.upload,
            sinon.match.same(this.params),
            sinon.match.func
        );
    }

    @test
    async "upload should resolve with response if successful"() {
        const data = { foo: 'bar' };
        s3.upload.callsArgWith(1, null, data);
        const result = await this.uploader.upload();
        Chai.expect(result).to.equal(data);
    }

    @test
    async "upload should reject with error if not"() {
        const error = new Error('Test Error');
        s3.upload.callsArgWith(1, error, null);
        try {
            await this.uploader.upload();
            throw new Error('Promise should have rejected.');
        } catch(err) {
            Chai.expect(err).to.equal(err);
        }
    }
}

如果我用 mocha 来做这件事,我会将每个方法的测试分组到一个嵌套describe块中。我不确定这是否受到鼓励甚至可能mocha-typescript,但如果是这样,您可能会考虑它。


推荐阅读