Search code examples
javascriptunit-testingasynchronousjasmine

Jasmine halts after 2nd call to the same async mock function in test function


I suspect Jasmine halts after running an async mocked function 2nd time in the same function, but I cant seem to find the reason or the correct workaround.

The getDialogAnswer is a jquery dialog wrapped in a promise and async await function. This is an implementaiton of "delete" + "are you sure" dialog boxes. Everything works as expected in the running application.

the 2 calls

let optiontext = 'Delete project from plan?'
let deleteProject = await getDialogAnswer(title, optiontext, choices, defaultvalue)
...
optiontext = 'REALLY delete project from plan???<br>There is no going back'
deleteProject =  await getDialogAnswer(title, optiontext, choices, defaultvalue)

The mock function

const getDialogAnswer = jasmine.createSpy('Mock_getDialogAnswer').and.returnValues('yes','yes');

The test function is created with async

it("should delete on yes + yes", async () => {

This passes

expect(getDialogAnswer).toHaveBeenCalledTimes(2)

but following 4 of this type fails - says called 0 times.

expect(mainPart.clearSelected).toHaveBeenCalledTimes(1)

The last I check is

expect(unsaved_changes).toBe(true);

which reports "Expected null to be true." But a console output writes correct true, so I know the function finished correctly.

The test is running in Jasmine HTML standalon Specrunner. I have tested with both 4.6,4.6 and 5.0Beta.

If I change the second call

deleteProject =  await getDialogAnswer(title, optiontext, choices, defaultvalue)

to

deleteProject =  'yes'

getDialogAnswer is of course only run once, but the rest of the expectaions pass!

And if I just remove the await of the second call to be like this

deleteProject =  getDialogAnswer(title, optiontext, choices, defaultvalue)

It all passes test - but then I cannot delete in the real application! Here the delete is not carried through.

I have gone through all I could find on async and spyes in the doc https://jasmine.github.io/index.html, and tried the method

const getDialogAnswer = jasmine.createSpy('Mock_getDialogAnswer').and.returnValues(
    Promise.resolve('yes'),Promise.resolve('yes'));

With same result

I have searched here, but mainly found how to setup test functions. I have a suspecion that I may have setup my mock fucntion incorrect or that Jasmine maybe has a flaw here. But I cant find any documentaion or other thread that shed some light on this. This might be a lead, but it seems to me that I am already doing it right. How to test async function with spyOn? Or am I missing something tiny but essential?


Solution

  • Solved the issue myself.

    The culprit was "missing" async/await keywords in application code. Not needed for function but for test only!

    By posting the question I apparently set some thoughts in motion! It sometimes help solving an issue by stating and explaining it to others!

    • Jasmine works flawlessly (at least in this matter)
    • My test case works correct (with the addition of returning resolved promise)

    This is the boiled down and scrambled original code, which works in the application, but not in test:
    I have had to insert the async block in the original object method, to make the 2 async/promise dialog calls work (And yes, messy and ripe for refactoring!)

        function main_obj() {
        this.objmethod1 = function () {
            const async_objmethod = async function(){
                let deleteProject = await getDialogAnswer()
                deleteProject = await getDialogAnswer()
                this.objmethod2();
                this.objmethod3()
                this.objmethod4();
                unsaved_changes = true;
                method5(this.name);
            }
            async_objmethod.bind(this)();
        };
        this.objmethod2 = function () {};
        this.objmethod3 = function () {};
        this.objmethod4 = function () {};
    }
    

    By adding async + await the test runs and passes as intended and expected

        this.objmethod1 = async function () {
            ...
            await async_objmethod.bind(this)();
        };
    

    In the application, there is no need to wait for the function to finish, but to test it properly it was needed.

    I have included working specrunner html, code + testcode for completenes

    specrunner.html

    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Jasmine Spec Runner</title>
      <link rel="shortcut icon" type="image/png" href="jasmine_lib/jasmine-5.0.0-beta.0/jasmine_favicon.png">
      <link rel="stylesheet" href="jasmine_lib/jasmine-5.0.0-beta.0/jasmine.css">
      <script src="jasmine_lib/jasmine-5.0.0-beta.0/jasmine.js"></script>
      <script src="jasmine_lib/jasmine-5.0.0-beta.0/jasmine-html.js"></script>
      <script src="jasmine_lib/jasmine-5.0.0-beta.0/boot0.js"></script>
      <script src="jasmine_lib/jasmine-5.0.0-beta.0/boot1.js"></script>
    
      <script src="maincode2.js" > </script> 
      <script src="test_spec.js"></script>
    
    </head>
    <body></body>
    </html>
    

    maincode.js

    function main_obj() {
        this.objmethod1 = async function () {
            const async_objmethod = async function(){
                let deleteProject = await getDialogAnswer()
                deleteProject = await getDialogAnswer()
                this.objmethod2();
                this.objmethod3()
                this.objmethod4();
                unsaved_changes = true;
                method5(this.name);
            }
            await async_objmethod.bind(this)();
        };
        this.objmethod2 = function () {};
        this.objmethod3 = function () {};
        this.objmethod4 = function () {};
    }
    

    test_spec.js

    const getDialogAnswer = jasmine.createSpy('Mock_getDialogAnswer').and.returnValue(
        Promise.resolve('yes'),
        Promise.resolve('yes'),
    );
    const method5 = jasmine.createSpy('Mock_method5')
    let unsaved_changes;
    
    // var for main_inst instance set in 'before-funcs'
    let main_inst
    beforeEach(function() {
        main_inst = new main_obj();
        unsaved_changes = null
        spyOn(main_inst, 'objmethod2')
        spyOn(main_inst, 'objmethod3')
        spyOn(main_inst, 'objmethod4')
    });
    
    describe("Test makemain_inst.js", () => {
        describe("Test new jquery ui dialog functions", () => {
            describe("test dialog uses", () => {
                describe("test this.objmethod1()", () => {
                    describe("test yes no", () => {
                        it("should delete on yes + yes", async () => {
                            // ------------ setup -------------
                            // --------   Call ----------------
                            const x = await main_inst.objmethod1()
                            // ------------- verify ------------
                            expect(getDialogAnswer).toHaveBeenCalledTimes(2)
    
                            expect(main_inst.objmethod2).toHaveBeenCalledTimes(1)
                            expect(main_inst.objmethod3).toHaveBeenCalledTimes(1)
                            expect(main_inst.objmethod4).toHaveBeenCalledTimes(1)
                            expect(method5).toHaveBeenCalledTimes(1)
                            expect(unsaved_changes).toBe(true);
                        });
                    });
                });
            });
        });
    });