3

我有一个 Express 应用程序,当某些端点被击中时,它使用node-slack-sdk向 Slack 发布帖子。我正在尝试为一条路线编写集成测试,其中包括从该库调用一个方法。

我想阻止 Slack 库中某些方法的所有默认行为,并简单地断言这些方法是使用某些参数调用的。

我试图简化问题。如何chat对 an 实例的方法(实际上嵌套在 中)进行存根WebClient,阻止原始功能,并对调用它的参数进行断言?

我已经尝试了很多没有用的东西,所以我正在编辑它并在这里提供一个大大简化的设置:

index.html

const express = require('express');
const {WebClient} = require('@slack/client');
const app = express();
const web = new WebClient('token');

app.post('/', (req, res) => {

    web.chat.postMessage({
        text: 'Hello world!',
        token: '123'
    })
        .then(() => {
            res.json({});
        })
        .catch(err => {
            res.sendStatus(500);
        });
});

module.exports = app;

index.test.html

'use strict';
const app = require('../index');
const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);

const {WebClient} = require('@slack/client');


describe('POST /', function() {
    before(function() {
        // replace WebClient with a simplified implementation, or replace the whole module.
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                // assert that web.chat.postMessage was called with {message: 'Hello world!'}, etc
        });
    });
});

与其他示例不同,有几件事使这变得困难。一,我们无法访问web测试中的实例,所以我们不能直接存根方法。第二,该方法隐藏在chat属性中web.chat.postMessage,这也与我在 sinon、proxyquire 等文档中看到的其他示例不同。

4

3 回答 3

3

您的示例设计不是很可测试,这就是您遇到这些问题的原因。为了使其更具可测试性和内聚性,最好传入您的 WebClient 对象和其他依赖项,而不是在您的路由中创建它们。

const express = require('express');
const {WebClient} = require('@slack/client');
const app = express();//you should be passing this in as well. But for the sake of this example i'll leave it


module.exports = function(webClient) {
   app.post('/', (req, res) => {

       web.chat.postMessage({
          text: 'Hello world!',
          token: '123'
       })
           .then(() => {
              res.json({});
           })
           .catch(err => {
              res.sendStatus(500);
           });
   })
   return app;
};

为了实现这一点,请在更高的模块中构建您的对象/路由。(您可能需要编辑为您生成的 express。我不确定,我个人使用经过大量重构的 express 版本来满足我的需要。)通过传入您的 WebClient,您现在可以为您的测试创建一个存根。

'use strict';

const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);
const {WebClient} = require('@slack/client');
const web = new WebClient('token');
let app = require('../index')(web);

describe('POST /', function() {

    it('should call chat.update with specific arguments', function() {
        const spy = sinon.spy();
        sinon.stub(web.chat, 'postMessage').callsFake(spy);

        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                assert(spy.calledWith({message: 'Hello world!'}));
        });
    });
});

这称为依赖注入。而不是让您的索引模块构建它的依赖项WebClient,您的更高模块将传递依赖项,以便它们控制它的较低模块的状态。您的较高模块,您的测试,现在拥有为较低模块索引创建存根所需的控制权。

上面的代码只是快速工作。我还没有测试过它是否有效,但它应该回答你的问题。

于 2018-04-24T16:58:31.577 回答
1

所以@Plee 在结构方面有一些优点。但我的回答更多是关于手头的问题,如何进行测试以及您需要了解的内容。为了更好地编写单元测试,你应该使用其他好的资源,比如书籍和文章,我认为网上会有很多很棒的资源可以用来做同样的事情

你在测试中做错的第一件事就是第一行本身

const app = require('../index');

这样做,你加载index文件,然后执行下面的代码

const {WebClient} = require('@slack/client');
const app = express();
const web = new WebClient('token');

所以现在模块已经加载了原始@slack/client文件并创建了一个在模块外无法访问的对象。所以我们失去了定制/监视/存根模块的机会。

所以第一条拇指规则

切勿在测试中全局加载此类模块。或者在存根之前永远不要加载它们

所以接下来我们想要的是在我们的测试中,我们应该加载我们想要存根的原始客户端库

'use strict';
const {WebClient} = require('@slack/client');
const sinon = require('sinon');

现在,由于我们无法在 中获取创建的对象index.js,因此我们需要在创建对象时对其进行捕获。这可以像下面这样完成

var current_client = null;

class MyWebClient extends WebClient {
    constructor(token, options) {
        super(token, options);
        current_client = this;
    }
}

require('@slack/client').WebClient = MyWebClient;

所以现在我们所做的就是将 originalWebClient替换为 our MyWebClient,当任何人创建相同的对象时,我们只需将其捕获在current_client. 这假设我们加载的模块只会创建一个对象。

接下来是更新我们的before方法以存根该web.chat.postMessage方法。所以我们更新我们的before方法如下

before(function() {
    current_client = null;
    app = require('../index');
    var stub = sinon.stub();
    stub.resolves({});
    current_client.chat.postMessage = stub;
});

现在是测试功能,我们更新如下

it('should call chat.update with specific arguments', function() {
    return chai.request(app).post('/').send({})
        .then(function(res) {
            expect(res).to.have.status(200);
            expect(current_client.chat.postMessage
                .getCall(0).args[0]).to.deep.equal({
                text: 'Hello world!',
                token: '123'
            });
        });
});

结果是积极的

结果

下面是index.test.js我用的完整的,你index.js的没变

'use strict';
const {WebClient} = require('@slack/client');
const sinon = require('sinon');

var current_client = null;

class MyWebClient extends WebClient {
    constructor(token, options) {
        super(token, options);
        current_client = this;
    }
}

require('@slack/client').WebClient = MyWebClient;

const chai = require('chai');
const chaiHttp = require('chai-http');

const expect = chai.expect;
chai.use(chaiHttp);


let app = null;
describe('POST /', function() {
    before(function() {
        current_client = null;
        app = require('../index');
        var stub = sinon.stub();
        stub.resolves({});
        current_client.chat.postMessage = stub;
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                expect(current_client.chat.postMessage
                    .getCall(0).args[0]).to.deep.equal({
                    text: 'Hello world!',
                    token: '123'
                });
            });
    });
});
于 2018-04-25T14:33:25.933 回答
1

根据其他评论,您似乎处于一个难以进行剧烈重构的代码库中。所以这就是我将如何测试而不对你的index.js.

我在这里使用rewireweb库从索引文件中获取并存根变量。

'use strict';

const rewire = require('rewire');
const app = rewire('../index');

const chai = require('chai');
const chaiHttp = require('chai-http');
const sinon = require('sinon');

const expect = chai.expect;
chai.use(chaiHttp);

const web = app.__get__('web');

describe('POST /', function() {
    beforeEach(function() {
        this.sandbox = sinon.sandbox.create();
        this.sandbox.stub(web.chat);
    });

    afterEach(function() {
        this.sandbox.restore();
    });

    it('should call chat.update with specific arguments', function() {
        return chai.request(app).post('/').send({})
            .then(function(res) {
                expect(res).to.have.status(200);
                const called = web.chat.postMessage.calledWith({message: 'Hello world!'});
                expect(called).to.be.true;
        });
    });
});
于 2018-04-27T03:57:40.947 回答