实例的存根方法

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

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

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

我尝试了很多没有奏效的东西,所以我正在编辑它,并在这里提供了一个极其简化的设置:

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等文档中看到的其他示例不同。


你的例子的设计不是很好测试,这就是为什么你有这些问题。 为了使其更具可测试性和内聚性,最好传入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;
};

为了实现这个,请在​​更高的模块上构建您的对象/路线。 (您可能需要编辑为您生成的快递,我不确定,我个人使用快速重构版快递来满足我的需求。)通过传入您的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,而是会传递依赖关系,以便它们控制低级模块的状态。 你的高级模块,你的测试,现在有了它需要的控制来为下层模块索引创建一个存根。

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


所以@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;

所以现在我们所做的是将原始的WebClient替换为我们的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'
                });
            });
    });
});

根据其他意见,看起来你是在一个代码库中进行剧烈的重构会很困难。 所以这里是我将如何测试而不对你的index.js做任何改变。

我在这里使用rewire库来获取并从索引文件中取出web变量。

'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;
        });
    });
});
链接地址: http://www.djcxy.com/p/82257.html

上一篇: Stub method of instance

下一篇: Should I hide DTOs and View Models behind interfaces or abstractions?