Overriding Core And Custom Services In AngularJS
Yesterday, I took a look at how to override directive definitions in AngularJS. As a quick follow-up, I thought I would demonstrate that the same kind of thing can be done with services. In AngularJS, of course, we can always use decorators to augment or replace services (and directives) at configuration time. But, we can also brute-force the replacement of services by redefining them in our modules. This applies to both custom services as well as to core AngularJS-provided services like $exceptionHandler.
Run this demo in my JavaScript Demos project on GitHub.
When it comes to service definitions, the last one wins. Meaning, no matter how many times you define a particular service, the last executed definition is the one that takes root in the application. To demonstrate this, I've created a small AngularJS application in which I am:
- Defining the same service three times.
- Redefining the core AngularJS service, $log.
The AppController then makes use of both of these overridden services.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Overriding Core And Custom Services In AngularJS
</title>
</head>
<body ng-controller="AppController">
<h1>
Overriding Core And Custom Services In AngularJS
</h1>
<p>
<em>See console for output.</em>
</p>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.4.5.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
angular.module( "Demo", [] );
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I control the root of the application.
angular.module( "Demo" ).controller(
"AppController",
function AppController( greeter, $log ) {
$log.log( greeter( "Sarah" ) );
}
);
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// **** NOTE: I am defining the same service - greeter - three times. This is
// **** to demonstrate that each definition of the service overwrites the
// **** previously-defined ones. Ultimately, only the last one defined will
// **** take root in the application.
// I provide a function that returns a greeting for the given name.
angular.module( "Demo" )
.factory(
"greeter",
function greeterFactory() {
return( greeter );
function greeter( name ) {
return( "Hello there, " + name + "?" );
}
}
)
.factory(
"greeter",
function greeterFactory() {
return( greeter );
function greeter( name ) {
return( "What it be like, " + name + "?" );
}
}
)
.factory(
"greeter",
function greeterFactory() {
return( greeter );
function greeter( name ) {
return( "What's going on, " + name + "?" );
}
}
)
;
// --------------------------------------------------------------------------- //
// --------------------------------------------------------------------------- //
// I override the CORE SERVICE $log.
// --
// NOTE: This is different from a decorator in that we are completely replacing
// the service and do not have access to the previously-defined versions (ie,
// the $delegate) of the services.
angular.module( "Demo" ).factory(
"$log",
function logFactory( $window ) {
// For the sake of simplicity, our override-$log service will only have
// a .log() method.
return({
log: function() {
var args = Array.prototype.slice.call( arguments );
args.unshift( "[ " + new Date().toDateString() + " ]" );
$window.console.log.apply( $window.console, args );
}
});
}
);
</script>
</body>
</html>
As you can see, each of the "greeter" services returns a different string. And the override of $log prepends the current date. And, when we run this code, we get the following output:
As you can see, the last executed definition of "greeter" is ultimately the one that gets instantiated by the dependency-injection mechanism and provided to the AppController.
While I don't know much about unit testing or integration testing, I suspect that testing is probably the scenario in which this full-on replacement of services makes sense. In non-testing use-cases, I would suggest just using a decorator to augment an existing service. Using a decorator, instead of a full-on replacement, is great because you receive a reference to the previously-instantiated version (the $delegate) which can always reference from within your override.
Want to use code from this post? Check out the license.
Reader Comments
The service-definition-replacement technique you describe here is one of my favorite ways to replace services with mocks, especially when I'm mocking that thing repeatedly across many test suites.
A good example is a logger service that presents popup toasts (John Papa's toastr). I don't want to see toasts popping up in my tests. So I create a helper function that I registers a toastr-less, spyable mock version with the module injector in a `beforeEach`. Problem solved once and for all my application tests.
An example of this may be found in my [bardjs](https://github.com/wardbell/bardjs) Angular testing library.
Thanks for bringing clarity to the service-redefinition subject.
@Ward,
Yeah, definitely time for me to learn more about testing.