Many AngularJS developers depend on $scope for implementing and testing its controllers’ logic. Even if this may bring performance and design problems, based on a series of articles and a conceptual test, in what-now I’ve tried to describe and put in practice what could be a solution.
Dollar-scope soup, hey!
♪ what a very good soup ♫
Some weeks ago my colleague Mariano Ravinale showed me an interesting article about AngularJS that deals with the existence of the “$scope soup”. This concept depicts the consequence of how AngularJS tutorials and documentation are being tackled, but they don’t show what the proper way of building the system is.
One of the first things one learns when starting working on AngularJS, is about the $scope variable and how it can be used to share information between a template and a controller. Besides, that dependency can also be injected in the tests. And sharing that tests-state with the real code, makes it easy for us to verify the inner state and to re-test or unit-test the methods being implemented.
However, the $scope service is easily and frequently abused by many programmers since, not only does it creates a terribly complex dependency relation between its variables, but it also generates a performance problem for its Angular application. This is so since, everything that depends from $scope will be verified in the digest phase.
In the original article, 5 guidelines for avoiding scope soup in Angular, the very first suggestion given is that of separating or ‘decoupling’ the methods that are part of the controllers from those methods and characteristics exposed in $scope. As the explanation given in the article on this issue is scarce, I think its worth to expand the idea now.
Controllers as self-contained classes
An unknown fact that is usually hidden on plain sight is that controllers in AngularJS are functions to which dependencies are injected to its constructor. This is equivalent to declaring a function that works for our class and to try and inject the dependencies in its constructors’ parameters.
How can unit testing be done without $scope?
And now that the necessary changes have been made, now that $scope is only restricted to the data sharing process, the question is how can testing be done when the controller state is no longer part of the $scope? Don’t panic! The first thing we must take into consideration is that when testing, we don’t want to evaluate every single variable or tiny detail of our controller. This would make our tests REALLY dependable of the current design and it would thus be impossible to make changes without rewriting the tests. The functions available will be those that the controller exposes, together with how the injected dependencies are used. If $scope is used to maintain a list, we can always verify that the list contains the desired values. We do not wish to validate a variable, but to share the contract with the template or the directive.
Three independent tasks: private, public and contractual
In this section we will try to define three very different and clearly marked areas of the controllers. On the one hand, it is easy to define a series of private functions that will only be accessible through the controller, where not even the testing may have access to. This makes sense since the controller doesn’t want to expose all its inner complexity, provided that its working units do their job. This is where testing should focus on: logic to work properly and not in variables to have certain values after certain calls.
The controller’s public area will be the main functionality that the controller offers. Even if it doesn’t count with any particular and characteristic syntax, I believe that it should be the only part of the controller to which a $scope should have access to, or one to which outcomes of other services or controllers should have access to. This can be tested by making the corresponding calls and by verifying the results. The inner logic should be isolated in the private area.
The contractual area is the one that allows the controller to interact with other services, including $scope. In this stage it is very important to verify that the variables have a specific name, because they will be used as a contract, and that they also have a certain value after a series of events.
Therefore, the so-called APIs should be done following the appropriate parameters. This will be tested by injecting Jasmine spies, or mocking services and making sure that the calls take place properly.
A practical example
As a four line example was not going to be enough, I decided to put this in practice in order to be able to draw some conclusions. In what-now I rewrote how What Now’s (example app) main controller works and in what ways those main controller tests work.
We welcome feedback as regards how it can be improved, or if you believe that this approach is right or wrong, please feel free to let us know. Send us your opinion to email@example.com