applications with ES6 modules
Testing of Angular JS application used to be quite painful especially when using “official” solutions like Karma or Protractor.
Check out the Github repository for implementation of the concepts described in this post.
Presentation about the concepts explained in this post.
A bit of history
The concept and value of testing should be obvious to all of those fortunate developers who have found themselves in situation where they were building any non-trivial Angular JS application.
The standard way of testing Angular JS application is using Karma test runner together with Jasmine test framework. Karma then runs tests written with help of Jasmine API in a browser. You can choose your preferred browser while developing locally, but the proper “fun” starts with a team of developers and one or more continuous integration environments. Usually the browser of choice becomes PhantomJS which tends to demonstrate rather quirky behavior with it’s two separated context (node.js and browser) and the dreaded timeouts.
Anatomy of a standard Jasmine test
Let’s check an example of a simple test which was written with help of above mentioned technologies.
As you can see, there is quite a lot of Angular JS API being used, mainly during the initialization of test context. But wait, it gets even better in case of controllers…
By the way, how many times did you called $rootScope.$digest(); in your tests? You know to test anything with promises…
So what’s the problem?
Hmm, let me start with …
- Angular context module(‘app’) must be instantiated to be able to do any testing at all even though the functionality may be in form of pure functions / classes not using any Angular specific API. Without Angular context you can’t get access (reference) to your controllers / services.
- Angular and all other used libraries must be included during testing so that it is even possible to instantiate Angular context (check out this example karma.conf file in official Angular Github repository, mainly the files property)
- Angular context can grow quite large so that it’s creation will consume considerable amount of time for every test file.
- Karma exclusion syntax doesn’t follow standard node glob pattern which can make you go crazy when you try to solve timeout errors caused by insufficient memory on PhantomJS by splitting test execution into multiple batches, while supporting dev mode single test execution (karma uses extra exclude property instead of supporting standard “!”)
I could definitively find more things that really grind my gears about Karma, Jasmine combo but the most important thing and the root of many other problems is described in the fist point of this list.
What is Angular context and why do we have to instantiate it for every test?
When I am writing about Angular context, what I really want to describe is initialized Angular JS dependency injection mechanism. I suppose that the original solution was somewhere along the lines of working with available technology and choosing lesser evil during the development of early Angular JS incarnation. The key points were:
- Need to support larger applications with respective large code bases
These two points led to implementation of original Angular JS dependency injection mechanism which can be summarized as:
Angular JS dependency mechanism works by registering everything on global angular object by using exposed API like module, controller, factory…
What does it mean for the tests?
To summarize, the many times mentioned Angular context is nothing else than initialized dependency injection mechanism. It can be quite small for a particular test if you split your app into multiple angular modules but never the less you still need Angular DI to at least get the access to the functionality you want to test.
Enter (ES6) module era
This particular approach is in fact achievable by any module system out there, but with the ES6 being officially released and also adopted by Typescript I would say it is the choice which makes most sense right now
To put it bluntly …
Isn’t that beautiful ? and cleaner, simpler, more concise, effective…
So what have we done there?
As you can see, test just imports the service and well… tests it! No Angular context or API whatsoever, just as it should be because we are unit testing the service functionality not the Angular’s dependency injection mechanism.
Dependencies (if needed) are passed explicitly as a parameter of function
Let’s continue with the test for controller…
Controller’s name is ‘SomeComponent’ because it follows the Component Pattern for Angular JS which can help a lot with subsequent migration to Angular 2.0.
Again if it has some dependencies, just pass them as a parameters to the function.
How does it work ?
We are using Mocha test runner together with chai assertion library. The tests are importing just the tested units (like controller or service function) and then the tests are simply executed. It’s fast! No browser, no Angular context, simple mocking by hand or using some of the available libraries like Sinonjs.
So the testing is easy but how do we integrate our functionality into Angular? The basic gist of it is to separate the functionality from the registering it into angular context into two separate files (modules). Service file then can look something like this:
And now it’s time to register it into Angular context…
We imported and registered both service and its dependency, the initialTodos constant. You can apply the same approach for all other Angular JS constructs like directives, controllers, …
Use the modules!
It’s good for you…
(ES6) Modules changed everything. Your controllers / classses / services / whatever are now just that. An easily testable function or class exported from a stand-alone module (file)
- Tests are simpler and faster
- Separation of implementation from Angular nudges you towards using less of soon to be deprecated Angular JS 1.X API like $scope, $watch, $on, …
- Mocha has quite good support for asynchronous testing with promises (like beforeEach will wait for resolution of promise before further execution if it was returned at the end of it’s function body)
- No implicitly available dependencies (from initialized Angular JS context) will make all mocking explicit which is more in direction of proper unit testing (or you can just require the original dependency’s implementation in the test and explicitly inject it into tested service)
- As for Angular 2.0, dependencies will be declared using Angular 2.0 @notations (which are implemented using ES7 decorators) so if you just import component’s class without Angular 2.0 context everything would be the same because the decorators will be inert without Angular 2.0 being present. Then again you will instantiate your classes and pass you dependencies explicitly, or the Angular guys will come with some much better solution
Integration & E2E testing …
While I haven’t used this pattern in any bigger project yet, it looks really similar to what we have done with the node.js modules in ongoing project. Basically integration testing would be executed by importing service with it’s whole dependency tree or even with Karma / Jasmine combo. They are quite good in initializing of Angular JS context, aren’t they? As for E2E tests, no change there… Protractor or Webdriver or whatever you like…
I also implemented an example Github repository using approach described in this post so you can easily check it out in action.
Yes, you made it to the end!
Don’t forget to recommend this article if you found it helpful and check out other interesting front-end and Angular JS related posts like…
Custom input formatting with simple directives for Angular 2
How to implement custom reusable input formatters with help of Angular 2 directives to enable simple & flexible drop-in…
A guide to simple, parametrized Webpack builds
Webpack is one of the most popular & effective build solutions available right now.
Follow me on Twitter to get notified about the newest blog posts and useful front-end stuff.