使用 postMessage 和 addEventListener 进行 Jasmine 单元测试

Posted

技术标签:

【中文标题】使用 postMessage 和 addEventListener 进行 Jasmine 单元测试【英文标题】:Jasmine Unit Test with postMessage and addEventListener 【发布时间】:2015-09-12 23:33:04 【问题描述】:

我正在尝试使用 postMessage 和 addEventListener 对情况进行单元测试。用例是我使用类似于 OAuth 工作流程的单独窗口进行用户登录,然后在登录窗口中使用 postMessage 通知主窗口用户已登录。这是主窗口中的监听代码:

$window.addEventListener("message", function(event) 
    if (event.data.type === "authLogin") 
        service.curUser = event.data.user;
        utilities.safeApply($rootScope);
        $window.postMessage(type: "authLoginSuccess", '*');
    
);

utilities.safeApply 函数定义为:

// Run $apply() if not already in digest phase.
utilitiesService.safeApply = function(scope, fn) 
    return (scope.$$phase || scope.$root.$$phase) ? scope.$eval(fn) : scope.$apply(fn);
;

我的单元测试是设计用来发送postMessage来模拟登录的:

describe('auth login postMessage', function() 
    var testUser = handle: 'test';
    beforeEach(function(done) 
        $window.postMessage(type: 'authLogin', user: testUser, '*');
        function onAuthLoginSuccess(event) 
            $window.removeEventListener('message', onAuthLoginSuccess);
            done();
        
        $window.addEventListener("message", onAuthLoginSuccess);
    );
    it("should set the user object", function() 
        expect(service.curUser).toEqual(testUser);
    );
);

这是运行单元测试的结果:

12 09 2015 14:10:02.952:INFO [launcher]: Starting browser Chrome
12 09 2015 14:10:05.527:INFO [Chrome 45.0.2454 (Mac OS X 10.10.5)]: Connected on socket 537CxfI4xPnR0yjLAAAA with id 12583721
Chrome 45.0.2454 (Mac OS X 10.10.5) ERROR
  Uncaught Error: Unexpected request: GET security/loginModal.tpl.html
  No more request expected
  at http://localhost:8089/__test/Users/userX/esupport/code/proj/public/vendor/angular-mocks/angular-mocks.js:250
Chrome 45.0.2454 (Mac OS X 10.10.5): Executed 23 of 100 (skipped 60) ERROR (0.333 secs / 0.263 secs)

我不知道为什么它会尝试加载 HTML 模板。我已经缩小了范围,这样如果我不调用 $scope.$apply() 函数,单元测试就会成功而不会出错。但是,我需要 $apply() 来更新视图。

我已经尝试在单元测试中删除 utility.safeApply 方法,并且还尝试为 HTML 模板 GET 请求设置期望值。这些尝试看起来像:

describe('auth login postMessage', function() 
    var testUser = handle: 'test';
    beforeEach(function(done) 
        $httpBackend.when('GET', 'security/loginModal.tpl.html').respond('');  // <-- NEW
        spyOn(utilities, 'safeApply').and.callFake(angular.noop);  // <-- NEW
        window.postMessage(type: 'authLogin', user: testUser, '*');
        function onAuthLoginSuccess(event) 
            window.removeEventListener('message', onAuthLoginSuccess);
            done();
        
        window.addEventListener("message", onAuthLoginSuccess);
    );
    it("should set the user object", function() 
        expect(service.curUser).toEqual(testUser);
    );
);

这两种尝试都无济于事。我仍然收到相同的错误消息。我已经尝试过其他调试步骤,例如对 $location.path() 使用 spyOn,因此它会返回一个示例值,例如“/fake”。在我直接测试服务方法而不使用 postMessage 触发服务代码的所有其他单元测试中,存根工作正常。然而,在 addEventListener 函数中,$location.path() 返回 "" 指向一个理论,即 addEventListener 函数在与单元测试准备的完全不同的实例中运行。这可以解释为什么没有使用存根函数以及为什么这个其他实例试图加载一个伪造的模板。这个discussion 也巩固了这个理论。

所以现在的问题是,我该如何让它发挥作用?即,如何让 addEventListener 函数在使用我的存根函数的同一实例中运行并且它不向 HTML 模板发出请求?

【问题讨论】:

模板请求很可能来自指令。你到底在这里测试什么(指令、工厂、控制器等)?您问题顶部的代码在哪里? 我正在测试一个名为“security”的工厂,其中包含与身份验证相关的方法。我发布了一个gist,其中包含该工厂的完整代码。在这个工厂中,它确实打开了一个使用模板的模式,但仅在某些情况下。我从这个工厂测试的目标代码不应该加载模板。 【参考方案1】:

我只是模拟所有外部部件并确保最终结果符合您的期望。

看看你的要点,你可能需要模拟一些更多的服务,但这应该足以测试 "authLogin" 消息事件。

describe('some test', function() 
    var $window, utilities, toaster, securityRetryQueue, service, listeners;

    beforeEach(function() 
        module('security.service', function($provide) 
            $provide.value('$window',
                $window = jasmine.createSpyObj('$window', ['addEventListener', 'postMessage']));
            $provide.value('utilities',
                utilities = jasmine.createSpyObj('utilities', ['safeApply']));
            $provide.value('toaster',
                toaster = jasmine.createSpyObj('toaster', ['pop']));
            $provide.value('securityRetryQueue',
                securityRetryQueue = jasmine.createSpyObj('securityRetryQueue', ['hasMore', 'retryReason']));

            // make sure you're not fetching actual data in a unit test
            securityRetryQueue.onItemAddedCallbacks = [];
            securityRetryQueue.hasMore.and.returnValue(false);

            $window.addEventListener.and.callFake(function(event, listener) 
                listeners[event] = listener;
            );
        );

        inject(function(security) 
            service = security;
        );
    );

    it('registers a "message" event listener', function() 
        expect($window.addEventListener).toHaveBeenCalledWith('message', listeners.message, false);
    );

    it('message event listener does stuff', inject(function($rootScope) 
        var event = 
            data: 
                type: 'authLogin',
                user: 'user'
            ,
            stopPropagation: jasmine.createSpy('event.stopPropagation')
        ;

        listeners.message(event);

        expect(service.curUser).toBe(event.data.user);
        expect(toaster.pop).toHaveBeenCalledWith('success', 'Successfully logged in.');
        expect(utilities.safeApply).toHaveBeenCalledWith($rootScope);
        expect($window.postMessage).toHaveBeenCalledWith(type: "authLoginSuccess", '*');
        expect(event.stopPropagation).toHaveBeenCalled();
    ));
);

【讨论】:

感谢您的帮助!这看起来很有希望,但我很难理解和使用它。我已经就我的不确定性发布了一个不同的问题。 ***.com/questions/32582000/…给我一两天的时间来了解更多信息,我会就这个答案提供反馈。 谢谢!我不知道注入模拟服务的 $provide 模式。很有用。而且您的侦听器事件捕获想法非常聪明。我不会那样做的。

以上是关于使用 postMessage 和 addEventListener 进行 Jasmine 单元测试的主要内容,如果未能解决你的问题,请参考以下文章

sendmessage和postmessage的区别

使用 postMessage 和 addEventListener 进行 Jasmine 单元测试

addEvent和removeEvent优化写法

delphi 中 postmessage 和sendmessage用法

MFC中使用PostMessage和SendMessage函数

sendmessage、postmessage和直接调用该对话框的方法,有何区别,那个好一点?