如何测试一个函数在另一个函数之前被调用

时间:2021-08-07 19:32:13

I have some tightly coupled legacy code that I want to cover with tests. Sometimes it's important to ensure that one mocked out method is called before another. A simplified example:

我有一些紧密耦合的遗留代码,我想用测试来覆盖。有时确保在另一个方法之前调用一个模拟方法很重要。一个简化的例子:

function PageManager(page) {
    this.page = page;
}
PageManager.prototype.openSettings = function(){
    this.page.open();
    this.page.setTitle("Settings");
};

In the test I can check that both open() and setTitle() are called:

在测试中,我可以检查open()和setTitle()是否都被调用:

describe("PageManager.openSettings()", function() {
    beforeEach(function() {
        this.page = jasmine.createSpyObj("MockPage", ["open", "setTitle"]);
        this.manager = new PageManager(this.page);
        this.manager.openSettings();
    });

    it("opens page", function() {
        expect(this.page.open).toHaveBeenCalledWith();
    });

    it("sets page title to 'Settings'", function() {
        expect(this.page.setTitle).toHaveBeenCalledWith("Settings");
    });
});

But setTitle() will only work after first calling open(). I'd like to check that first page.open() is called, followed by setTitle(). I'd like to write something like this:

但是setTitle()只能在第一次调用open()之后才能工作。我想检查第一个page.open()是否被调用,然后是setTitle()。我想写这样的东西:

it("opens page before setting title", function() {
    expect(this.page.open).toHaveBeenCalledBefore(this.page.setTitle);
});

But Jasmine doesn't seem to have such functionality built in.

但Jasmine似乎没有内置这样的功能。

I can hack up something like this:

我可以破解这样的东西:

beforeEach(function() {
    this.page = jasmine.createSpyObj("MockPage", ["open", "setTitle"]);
    this.manager = new PageManager(this.page);

    // track the order of methods called
    this.calls = [];
    this.page.open.and.callFake(function() {
        this.calls.push("open");
    }.bind(this));
    this.page.setTitle.and.callFake(function() {
        this.calls.push("setTitle");
    }.bind(this));

    this.manager.openSettings();
});

it("opens page before setting title", function() {
    expect(this.calls).toEqual(["open", "setTitle"]);
});

This works, but I'm wondering whether there is some simpler way to achieve this. Or some nice way to generalize this so I wouldn't need to duplicate this code in other tests.

这有效,但我想知道是否有一些更简单的方法来实现这一点。或者一些很好的方法来概括这一点,所以我不需要在其他测试中复制此代码。

PS. Of course the right way is to refactor the code to eliminate this kind of temporal coupling. It might not always be possible though, e.g. when interfacing with third party libraries. Anyway... I'd like to first cover the existing code with tests, modifying it as little as possible, before delving into further refactorings.

PS。当然,正确的方法是重构代码以消除这种时间耦合。但是,它可能并不总是可能的,例如,与第三方库连接时。无论如何...我想首先用测试覆盖现有代码,尽可能少地修改它,然后再深入研究进一步的重构。

6 个解决方案

#1


4  

Try this:

it("setTitle is invoked after open", function() {
    var orderCop = jasmine.createSpy('orderCop');
    this.page.open = jasmine.createSpy('openSpy').and.callFake(function() {
        orderCop('fisrtInvoke');
    });

    this.page.setTitle = jasmine.createSpy('setTitleSpy').and.callFake(function() {
        orderCop('secondInvoke');
    });

    this.manager.openSettings();

    expect(orderCop.calls.count()).toBe(2);
    expect(orderCop.calls.first().args[0]).toBe('firstInvoke');
    expect(orderCop.calls.mostRecent().args[0]).toBe('secondInvoke');
}

EDIT: I just realized my original answer is effectively the same as the hack you mentioned in the question but with more overhead in setting up a spy. It's probably simpler doing it with your "hack" way:

编辑:我刚刚意识到我的原始答案实际上与你在问题中提到的黑客一样,但是在设置间谍方面有更多的开销。用你的“黑客”方式做这件事可能更简单:

it("setTitle is invoked after open", function() {
    var orderCop = []
    this.page.open = jasmine.createSpy('openSpy').and.callFake(function() {
        orderCop.push('fisrtInvoke');
    });

    this.page.setTitle = jasmine.createSpy('setTitleSpy').and.callFake(function() {
        orderCop.push('secondInvoke');
    });

    this.manager.openSettings();

    expect(orderCop.length).toBe(2);
    expect(orderCop[0]).toBe('firstInvoke');
    expect(orderCop[1]).toBe('secondInvoke');
}

#2


2  

I'd like to write something like this:

我想写这样的东西:

it("opens page before setting title", function() {
    expect(this.page.open).toHaveBeenCalledBefore(this.page.setTitle);
});

But Jasmine doesn't seem to have such functionality built in.

但Jasmine似乎没有内置这样的功能。

Looks like the Jasmine folks saw this post, because this functionality exists. I'm not sure how long it's been around -- all of their API docs back to 2.6 mention it, though none of their archived older style docs mention it.

看起来像Jasmine人看过这篇文章,因为这个功能存在。我不知道它已经存在了多长时间 - 所有的API文档都回到了2.6提到它,尽管它们的存档旧版文档都没有提到它。

toHaveBeenCalledBefore(expected)
expect the actual value (a Spy) to have been called before another Spy.

toHaveBeenCalledBefore(预期)期望在另一个间谍之前调用实际值(一个间谍)。

Parameters:

Name        Type    Description
expected    Spy     Spy that should have been called after the actual Spy.

A failure for your example looks like Expected spy open to have been called before spy setTitle.

您的示例失败,看起来像是在spy setTitle之前调用了Expected spy open。

#3


2  

Create a fake function for the second call that expects the first call to have been made

为第二次调用创建一个假函数,该函数需要进行第一次调用

it("opens page before setting title", function() {

    // When page.setTitle is called, ensure that page.open has already been called
    this.page.setTitle.and.callFake(function() {
        expect(this.page.open).toHaveBeenCalled();
    })

    this.manager.openSettings();
});

#4


0  

Inspect the specific calls by using the .calls.first() and .calls.mostRecent() methods on the spy.

使用间谍上的.calls.first()和.calls.mostRecent()方法检查特定的调用。

#5


0  

Basically did the same thing. I felt confident doing this because I mocked out the function behaviors with fully synchronous implementations.

基本上做了同样的事情。我有信心这样做,因为我用完全同步的实现模拟了函数行为。

it 'should invoke an options pre-mixing hook before a mixin pre-mixing hook', ->
    call_sequence = []

    mix_opts = {premixing_hook: -> call_sequence.push 1}
    @mixin.premixing_hook = -> call_sequence.push 2

    spyOn(mix_opts, 'premixing_hook').and.callThrough()
    spyOn(@mixin, 'premixing_hook').and.callThrough()

    class Example
    Example.mixinto_proto @mixin, mix_opts, ['arg1', 'arg2']

    expect(mix_opts.premixing_hook).toHaveBeenCalledWith(['arg1', 'arg2'])
    expect(@mixin.premixing_hook).toHaveBeenCalledWith(['arg1', 'arg2'])
    expect(call_sequence).toEqual [1, 2]

#6


0  

Lately I've developed a replacement for Jasmine spies, called strict-spies, which solves this problem among many others:

最近我开发了一个名为strict-spies的Jasmine间谍的替代品,它解决了许多其他问题:

describe("PageManager.openSettings()", function() {
    beforeEach(function() {
        this.spies = new StrictSpies();
        this.page = this.spies.createObj("MockPage", ["open", "setTitle"]);

        this.manager = new PageManager(this.page);
        this.manager.openSettings();
    });

    it("opens page and sets title to 'Settings'", function() {
        expect(this.spies).toHaveCalls([
            ["open"],
            ["setTitle", "Settings"],
        ]);
    });
});

#1


4  

Try this:

it("setTitle is invoked after open", function() {
    var orderCop = jasmine.createSpy('orderCop');
    this.page.open = jasmine.createSpy('openSpy').and.callFake(function() {
        orderCop('fisrtInvoke');
    });

    this.page.setTitle = jasmine.createSpy('setTitleSpy').and.callFake(function() {
        orderCop('secondInvoke');
    });

    this.manager.openSettings();

    expect(orderCop.calls.count()).toBe(2);
    expect(orderCop.calls.first().args[0]).toBe('firstInvoke');
    expect(orderCop.calls.mostRecent().args[0]).toBe('secondInvoke');
}

EDIT: I just realized my original answer is effectively the same as the hack you mentioned in the question but with more overhead in setting up a spy. It's probably simpler doing it with your "hack" way:

编辑:我刚刚意识到我的原始答案实际上与你在问题中提到的黑客一样,但是在设置间谍方面有更多的开销。用你的“黑客”方式做这件事可能更简单:

it("setTitle is invoked after open", function() {
    var orderCop = []
    this.page.open = jasmine.createSpy('openSpy').and.callFake(function() {
        orderCop.push('fisrtInvoke');
    });

    this.page.setTitle = jasmine.createSpy('setTitleSpy').and.callFake(function() {
        orderCop.push('secondInvoke');
    });

    this.manager.openSettings();

    expect(orderCop.length).toBe(2);
    expect(orderCop[0]).toBe('firstInvoke');
    expect(orderCop[1]).toBe('secondInvoke');
}

#2


2  

I'd like to write something like this:

我想写这样的东西:

it("opens page before setting title", function() {
    expect(this.page.open).toHaveBeenCalledBefore(this.page.setTitle);
});

But Jasmine doesn't seem to have such functionality built in.

但Jasmine似乎没有内置这样的功能。

Looks like the Jasmine folks saw this post, because this functionality exists. I'm not sure how long it's been around -- all of their API docs back to 2.6 mention it, though none of their archived older style docs mention it.

看起来像Jasmine人看过这篇文章,因为这个功能存在。我不知道它已经存在了多长时间 - 所有的API文档都回到了2.6提到它,尽管它们的存档旧版文档都没有提到它。

toHaveBeenCalledBefore(expected)
expect the actual value (a Spy) to have been called before another Spy.

toHaveBeenCalledBefore(预期)期望在另一个间谍之前调用实际值(一个间谍)。

Parameters:

Name        Type    Description
expected    Spy     Spy that should have been called after the actual Spy.

A failure for your example looks like Expected spy open to have been called before spy setTitle.

您的示例失败,看起来像是在spy setTitle之前调用了Expected spy open。

#3


2  

Create a fake function for the second call that expects the first call to have been made

为第二次调用创建一个假函数,该函数需要进行第一次调用

it("opens page before setting title", function() {

    // When page.setTitle is called, ensure that page.open has already been called
    this.page.setTitle.and.callFake(function() {
        expect(this.page.open).toHaveBeenCalled();
    })

    this.manager.openSettings();
});

#4


0  

Inspect the specific calls by using the .calls.first() and .calls.mostRecent() methods on the spy.

使用间谍上的.calls.first()和.calls.mostRecent()方法检查特定的调用。

#5


0  

Basically did the same thing. I felt confident doing this because I mocked out the function behaviors with fully synchronous implementations.

基本上做了同样的事情。我有信心这样做,因为我用完全同步的实现模拟了函数行为。

it 'should invoke an options pre-mixing hook before a mixin pre-mixing hook', ->
    call_sequence = []

    mix_opts = {premixing_hook: -> call_sequence.push 1}
    @mixin.premixing_hook = -> call_sequence.push 2

    spyOn(mix_opts, 'premixing_hook').and.callThrough()
    spyOn(@mixin, 'premixing_hook').and.callThrough()

    class Example
    Example.mixinto_proto @mixin, mix_opts, ['arg1', 'arg2']

    expect(mix_opts.premixing_hook).toHaveBeenCalledWith(['arg1', 'arg2'])
    expect(@mixin.premixing_hook).toHaveBeenCalledWith(['arg1', 'arg2'])
    expect(call_sequence).toEqual [1, 2]

#6


0  

Lately I've developed a replacement for Jasmine spies, called strict-spies, which solves this problem among many others:

最近我开发了一个名为strict-spies的Jasmine间谍的替代品,它解决了许多其他问题:

describe("PageManager.openSettings()", function() {
    beforeEach(function() {
        this.spies = new StrictSpies();
        this.page = this.spies.createObj("MockPage", ["open", "setTitle"]);

        this.manager = new PageManager(this.page);
        this.manager.openSettings();
    });

    it("opens page and sets title to 'Settings'", function() {
        expect(this.spies).toHaveCalls([
            ["open"],
            ["setTitle", "Settings"],
        ]);
    });
});