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

10

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

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

在测试中,我可以检查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");
    });
});

但是setTitle()只有在首先调用open()之后才能起作用。我想先检查是否调用了page.open(),然后再调用setTitle()。我想写出像这样的代码:

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

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

我可以编写类似以下代码的东西:

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"]);
});

这个方法可以实现,但我想知道是否有更简单的方法来完成。或者有没有一些好的方法来概括这个问题,以便我不需要在其他测试中重复这段代码。

附言:当然,正确的方法是重构代码,消除这种暂时性耦合。但有时可能不可行,例如在与第三方库进行接口时。无论如何……在深入进行进一步的重构之前,我想先对现有的代码进行测试,并尽可能少地修改它。


除了调用 open 之外,您还可以断言其他内容吗?比如 DOM 节点的更改或其他“全局”数据? - Henrik Andersson
不,重点是我正在模拟它 - 我不希望实际的“open”被调用,特别是因为它可能会影响一些全局状态。 - Rene Saarsoo
可能是有没有办法使用Jasmine验证间谍执行顺序?的重复问题。 - carpeliam
6个回答

8

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的开发者们看到了这篇文章,因为这个功能已经存在。我不确定它已经存在多久 - 所有他们API文档从2.6开始都提到了它,尽管没有一个存档的旧样式文档提到了它。

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

Parameters:

Name        Type    Description
expected    Spy     Spy that should have been called after the actual Spy.
你的例子中的失败看起来像这样:期望 spy open 在 spy setTitle 之前被调用

5

试试这个:

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');
}

编辑:我刚刚意识到我的原始答案实际上与您在问题中提到的hack方式基本相同,但需要更多的开销来设置间谍。也许用你的“hack”方法更简单:

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');
}

3
创建一个虚假函数,用于第二个调用,该函数期望第一次调用已经完成。请保留HTML标记。
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();
});

0

当比较一个方法的不同调用时,这仅有所帮助 - 例如 foo() 被传入参数 "a""b" 调用的先后顺序 - 但是对于决定 foo() 方法是否在 bar() 方法之前被调用则没有任何帮助。 - Rene Saarsoo

0

基本上做了同样的事情。我感到自信是因为我使用完全同步的实现来模拟函数行为。

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]

0
最近,我开发了一个取代Jasmine间谍的工具,称为strict-spies,它解决了很多问题,其中包括此问题:
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"],
        ]);
    });
});

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接