Ext.data.JsonP.testing_controllers({"title":"Unit testing MVC Controllers","guide":"
Contents
\n\nControllers are the part of the MVC\napplication architecture that execute the application logic such as responding\nto events and handling the business logic for your application.
\n\nUnit testing Controllers is complicated and resembles integration testing in\nthat it involves testing many components at once. It is\nimportant to simplify the testing process as much as possible, breaking the\ncomponent interaction down to the smallest reasonable pieces so that you only\nneed to debug a small piece of code when tests fail.
\n\nThe most important parts of a Controller are its refs and component selectors;\nit is crucial to ensure that these selectors are tested properly. Selectors are\none of the hardest things to test because they rely on the existence and\nparticular layout of the components they select.
\n\nSuppose that the application contains the following View and Controller:
\n\nExt.define('MyApp.view.MyView', {\n extend: 'Ext.panel.Panel',\n alias: 'widget.myview',\n\n dockedItems: [{\n xtype: 'button',\n text: 'OK',\n dock: 'bottom'\n }, {\n xtype: 'button',\n text: 'Cancel',\n dock: 'bottom'\n }],\n\n ...\n});\n\nExt.define('MyApp.controller.MyController', {\n extend: 'Ext.app.Controller',\n\n views: [\n 'MyView'\n ],\n\n refs: [{\n ref: 'myView', selector: 'myview'\n }, {\n ref: 'myViewButtonOk',\n selector: 'myview > button[text=OK]'\n }, {\n ref: 'myViewButtonCancel',\n selector: 'myview > button[text=Cancel]'\n }],\n\n init: function() {\n this.control({\n 'myview > button': {\n click: 'onMyViewButtonClick'\n }\n });\n }\n\n onMyViewButtonClick: function(button) {\n ...\n }\n});\n
\n\nFor this simplified example of a test suite, we will use the\nJasmine framework.\nSee Unit Testing with Jasmine for background information.
\n\nOur test spec must call each possible selector defined for the Controller so\nlooks something like this:
\n\ndescribe('MyController refs', function() {\n var view = new MyApp.view.MyView({ renderTo: Ext.getBody() }),\n ctrl = new MyApp.controller.MyController();\n\n it('should ref MyView objects', function() {\n var cmp = ctrl.getMyView();\n\n expect(cmp).toBeDefined();\n });\n\n it('should ref MyView button OK', function() {\n var btn = ctrl.getMyViewButtonOk();\n\n expect(btn.text).toBe('OK');\n });\n\n it('should ref MyView button Cancel', function() {\n var btn = ctrl.getMyViewButtonCancel();\n\n expect(btn.text).toBe('Cancel');\n });\n});\n
\n\nThis test suite is simplified to be easier to understand; it can be further\nshortened by auto-generating ref tests against the controller's refs array, etc.\nBut the central concept remains the same: we take an instantiated View and a\nController and run through all the possible refs, comparing returned objects to\nour expectations.
\n\nTaking the same View/Controller setup, we can now add a spec to test component\nselectors:
\n\ndescribe('MyController component selectors', function() {\n var view = new MyApp.view.MyView({ renderTo: Ext.getBody() }),\n ctrl = new MyApp.controller.MyController();\n\n it('should initialize', function() {\n ctrl.init();\n });\n\n it('should control MyView button click events', function() {\n spyOn(ctrl, 'onMyViewButtonClick');\n\n view.down('button[text=OK]').fireEvent('click');\n\n expect(ctrl.onMyViewButtonClick).toHaveBeenCalled();\n });\n});\n
\n\nNote that our Controller's init
method is called automatically when the\napplication is run but we must call the init
method manually in our test\nsuite. An empty spec works just fine and always passes.
This approach may not be feasible for larger applications and bigger Views;\nin that case, it may be beneficial to create mockup components that simulate\nparts of the component layout without adhering strictly to visual design. In\nfact, the test View above may be seen as an example of such a mockup for a real\nworld View.
\n\nEvent domains are a new concept introduced in\nExt JS 4.2; they allow passing information between application components\nwithout explicitly calling object methods. Remember that Controllers generally\nlisten for events and then execute the appropriate actions in response to those\nevents.
\n\nTo test the event domain selectors:
\n\nonFooEvent
in\nthis example) to react to events passed between Controllers; use the *
\nwildcard so that the selector matches any Controller.fooevent
event in the Controller instance to be tested.onFooEvent
method with the supplied arguments.Sample code to define the fooevent
handler function is:
Ext.define('MyApp.controller.MyController', {\n extend: 'Ext.app.Controller',\n\n init: function() {\n this.listen({\n // This domain passes events between Controllers\n controller: {\n // This selector matches any Controller\n '*': {\n fooevent: 'onFooEvent'\n }\n }\n });\n },\n\n onFooEvent: function() {}\n});\n
\n\nAfter initializing the MyController
instance, we can just fire fooevent
in\nany Controller instance (including itself) to execute the onFooEvent
method\nwith the supplied arguments.
Sample code to test this configuration is:
\n\ndescribe('MyController event domain selectors', function() {\n var ctrl = new MyApp.controller.MyController();\n\n it('should listen to fooevent in controller domain', function() {\n spyOn(ctrl, 'onFooEvent');\n\n ctrl.fireEvent('fooevent');\n\n expect(ctrl.onFooEvent).toHaveBeenCalled();\n });\n});\n
\n\nNotice how we fired fooevent
on the same Controller that is supposed to listen\nto this event? That is one of the side effects of how event domains work, and it\nis very useful for testing. However it does not help when we want to listen for\nfooevent
to be fired from a particular Controller instead of from just any\nController. To handle this, we can rewrite the test suite to define fooevent
\nspecifically for each controller:
Ext.define('MyApp.controller.MyController', {\n extend: 'Ext.app.Controller',\n\n init: function() {\n this.listen({\n controller: {\n '#MyOtherController': {\n fooevent: 'onMyOtherControllerFooEvent'\n }\n }\n });\n },\n\n onMyOtherControllerFooEvent: function() {}\n});\n\nExt.define('MyApp.controller.MyOtherController', {\n extend: 'Ext.app.Controller',\n\n someMethod: function() {\n this.fireEvent('fooevent');\n }\n});\n
\n\nIn this case we must mock the MyOtherController
class in our test suite,\nto avoid instantiating it and loading its dependencies:
describe('MyController event domain selectors', function() {\n var ctrl1 = new MyApp.controller.MyController(),\n ctrl2 = new MyApp.controller.MyOtherController();\n\n it('should listen to fooevent from MyOtherController', function() {\n spyOn(ctrl, 'onMyOtherControllerFooEvent');\n\n // We do not execute MyOtherController.someMethod but fire fooevent\n // directly, because in a real world Controller someMethod may do\n // something useful besides just firing an event, and we only want\n // to test the event domain selector\n ctrl2.fireEvent('fooevent');\n\n expect(ctrl.onMyOtherControllerFooEvent).toHaveBeenCalled();\n });\n});\n
\n\nThis mockup works because the Controller's id
defaults to the last part of its\nclass name, unless it is specifically overridden.
Besides other Controllers' events, it is possible to listen
to Stores',\nExt.Direct Providers' and global events. See Ext.app.Controller.listen\nfor details about how to use event domains to test other elements of your\napplication; testing them is similar to testing the Controller's event domain.