简体   繁体   中英

How to test that one function is called before another

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:

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:

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.

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, eg 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.

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

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.

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.

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. 

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

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

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

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]

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

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM