Showing posts with label AngularJS. Show all posts
Showing posts with label AngularJS. Show all posts

Spying on AngularJS Services That Are called Directly as Functions

english mobile

Jasmine spies are great. But when testing AngularJS services, it can really be be hard to test that the correct calls are being made to services where you’re calling the service itself instead of an attached method, such as the $interval service. When you’re testing controllers, you can directly inject your own spy if you want to test that $interval was called with the correct arguments, but how do you get hold of a function that’s not attached to an object to be able to spy on it?
It turns out that your only option is to replace the service with your spy in the config phase with either $provide.decorator, or module.decorator. For your TL;DR convenience, here is a working module that you can add to the list of modules you instantiate before each test. I’ll explain why all this works after the code.

angular.module('spies.interval', [])
    .decorator('$interval', function($delegate){
       var spy = jasmine.createSpy(‘$interval’).and.callFake(
           function()  {
               return  $delegate.apply(arguments);
           }
       )
       spy.cancel =  angular.bind($delegate, $delegate.cancel);
       spy.flush =  angular.bind($delegate, $delegate.flush);
       spyOn(spy,  'cancel').and.callThrough();
       return spy;
    })

The spy game

Let’s first think about what happens when you call spyOn(someObj, ‘someFunction’), so we can understand why we’re not using that syntax here. Jasmine looks at someObj. If someObj has a function called ‘someFunction’, jasmine overwrites that function with a generic spy which may or may not call the original someFunction. But without someObj, there is nothing to attach that replacement function to to make the replacement invisible to the calling code under test.

Knowing when to delegate

So we have to “trick” Angular into injecting our replacement into the code under test when the code asks for the $interval service. We do this by returning the actual spy rather than an altered $delegate.
This then becomes very different from injecting a fake $interval service into a controller that’s created for just one test, because when you call $interval(), it caches the method to call after the elapsed time, the elapsed time, and so forth internally so that it can work its magic. So if you only replace $interval with a spy, you can no longer test that the right things happen after the time has elapsed, because that internal state isn’t right.
So we need to go on to call the actual $interval service, and we need to call it with the arguments that our service under test is using, which is why we use apply(). Once we’ve done that, we need to make sure to return the result of the call, a promise, so that when we test that code that should be canceled in our client code does not run, that doesn’t break because there’s no promise to give cancel().
Once you’ve taken all of the above into account, you have
var spy = jasmine.createSpy(‘$interval’).and.callFake(
    function() {
        return $delegate.apply(arguments);
})

Spies upon spies

 But you can’t call cancel yet (or flush), because the spy you’re replacing $interval with doesn’t have a cancel() method or a flush() method. If you try just directly adding the functions (spy.cancel = $delegate.cancel), the functions don’t run in the same scope that we wrote to when we called $delegate.apply().  By using angular.bind, we force them into the same scope as the delegate like so:
spy.cancel = angular.bind($delegate, $delegate.cancel);
spy.flush = angular.bind($delegate, $delegate.flush);
Finally, we spy on the cancel function. This is just a convenience—you can of course spy on this function as normal from any test.
spyOn(spy, 'cancel').and.callThrough();
And that, folks, is how you create a spy to be able to check that an angular service where the function is called directly was called with the correct methods without breaking the service.

But wait, there’s more

If you’re a Jasmine expert, you’re probably thinking that I could have just used the undocumented second argument of createSpy, which simplifies the code to this:
angular.module('spies.interval', [])
     .decorator('$interval', function($delegate){
        var spy =  jasmine.createSpy('$interval', $delegate);
        spyOn(spy,  'cancel').and.callThrough();
        return spy;    
})
Unfortunately, that doesn’t work. You would expect the Strategy to do the same thing internally that my home-rolled version does, but it doesn’t. Maybe some other day I’ll dig through the code and blog about why.

How useful is this anyway?

I think the main reason to use this is when the amount of time you need to wait or the optional arguments at the end will vary based on some data. However, your test usually can’t even see the method that ultimately gets called by $interval. So it feels a bit like it should not need to know so much about it that it is looking at what arguments are being passed to this function it can’t see.
So after all this, I ultimately came to feel that though you can do this, you probably shouldn’t, because it introduces a lot of coupling between your test and the internals of the methods in your service that you’re trying to test. For example, your test should not need to know whether your service internally creates its own closure to store state to call the method from in $interval or whether it directly stores that state in the end parameters of $interval.
In Scotland, they have a saying that a gentleman is a man who knows how to play the bagpipes, but doesn’t. I often feel that programming is like that.

How I learned to stop hating the command line (so much)

english mobile

I'm old. So I still remember when every computer screen looked like what we now call "the command line." Screens were black and text was green or amber. Anything you needed to do on the computer took forever and depended on your ability to remember lots of commands and type them correctly. This made computing seem difficult and unapproachable. So when the first MacIntosh computers showed up in the Art Department where I was studying, I was as happy as anyone.

Finally, it was (more or less) obvious how the computer was organized, and figuring out how to do things and what you could do was easy and intuitive. When I first used a Windows 3.1 computer, I was even more delighted. And so I have been a relatively happy camper for many years. Of course there have been difficulties and frustrations, but by and large if there was any task worth doing someone had built an interface that made it easy to do things without having to remember tedious commands and type them accurately. This allowed me to reserve my head space for the problems that actually interested me and/or I had been hired to solve.

I recently started learning AngularJS, and I was dismayed to find that before you can even start with Angular, you have to do a fair amount of work in the command line. Not only that, but there's little explanation of why you need to do these steps and no explanation of whether there are alternate routes to accomplish the same thing. I get that when you're trying to write a tutorial if you try to cover every last bit of background information someone might want to know that you'd never get finished.

However, the absence of such supporting information right now means that if you want to learn AngularJS (and I suspect many other current JavaScript frameworks) you're stuck using the command line whether you like it or not. You may not be surprised to learn that I don't especially like it. I've learned a few things over the past couple of months that make me dislike it a lot less. So if you share my dislike of the command line, keep reading and maybe we can make life a little easier.

Why we need the command line

As my elementary school teachers could tell you, I've never been especially fond of just doing things without knowing why. So let's start with why it is that it seems everything needs to go through the command line. The answer is simple (but seldom articulated): nodejs. Node was designed as a server-side technology, so the only facility it has for drawing to the screen is a browser that's pointed to a site that it's serving. However, most of the tools you use during JS development aren't conceived that way and I have no idea if they could be.

You get a few things from node that nearly make its lack of a decent UI worth it.

  1. Node lets you run a webserver (or several) out of any directory on your computer. Unlike more traditional web servers such as IIS or Apache, Node lets you just fire up a server anywhere. Security considerations aside, it's pretty cool that you're now free to put your code in whatever directory makes sense for development rather than the directory that the web server can see (or that you've set up as a virtual directory). You can even have one server running the site and another server running the unit tests.
  2. Node Package Manager (npm). Npm is like a magic registry in the sky that will allow you to more or less instantly download and install most tools and libraries you'll need for doing Javascript development. It will also make sure that if those tools have dependencies that the libraries they depend on are also installed, even if the different tools depend on different versions of the same library.
  3. Many of the tools are also in node or at least depend on it/run on top of it. For example, Karma test runner launches a node server on its own port to run the tests.

So we kind of have to use it, or at least get enough benefit from it that we can grit our teeth and use the command line. I had several pain points that were driving me flat up the wall, so I'd like to share some solutions that made things easier for me.

Navigating to directories

Directories to me are only significant as a way to keep things organized and insofar as they affect whether my code runs properly or not. It's not even on my radar to be interested in constantly typing (correctly, no less) CD and the full path to the directory where my code lives. So yes, I know from a stint doing embedded development on a Linux device a few years ago that to some extent you can get the CLI to type paths for you by typing the first letter or two of each directory and pressing the tab key. But that's only slightly less tedious. After several days of this, I got really annoyed and went searching.

What I found is that on Windows you can hold down the shift key while right-clicking on a folder. This adds "Open a command prompt here" to the context menu. Note that this is in a different spot on the context menu depending if you right-click on the left pane (shown) or the right pane.

Mac/Linux people, I am sure Google can tell you similar shortcuts for your environment. The point is you don't have to type the path over and over.

Typing the same crap over and over

If I enjoyed typing the same crap over and over, I probably wouldn't have decided to be a programmer. But as I got into the red-green rhythm of testing, I found myself typing karma start (red), karma start (green), cls (if the test passed, clear the screen and on to the next test).

Again, my vague memories came to the rescue and I remembered that you can use your up and down arrow keys to rerun the same commands again. So, once I'd typed karma start once, I could run it again by pressing the up arrow key once and pressing Enter. Then, after typing cls, two up arrows goes back past cls to karma start. From there, one up arrow goes to karma start again, or a down arrow goes to cls.

Yes, Mac users, I know that you use a different command to clear your screen, but I don't know it off the top of my head. Usually I run bootcamp on any Mac I use. But the idea is the same.

Not being able to read the text

Like I said, I'm old. I also enjoy sparring in karate, so my glasses may not sit exactly where they're supposed to on my head. So the red karma uses for a failed test is completely illegible to me on the default black background. Karma has about four colors I have seen:

  • Medium gray for ordinary text
  • Green for passing test
  • Red for failing test
  • Teal for console.log

I vaguely think I remember seeing yellow, but I don't remember what they were doing with it.

All this variability means that it can be tough to find a background color that works. Let's not talk text colors, because node and karma change them anyway. In karma, you have the choice of "use colors" or "don't use colors," but there is no "use better colors." Anyway, my experience is that a white background gives the best legibility across the most frequently-used colors. It's a shame, because it seems that "real developers" use darker color schemes, but there you have it.

One problem with setting the background color is that any time you run npm install, node will helpfully change your background color to black. I haven't found any way to get it to stop doing this, but if you set your defaults, you can just close the window and reopen it using the right-click menu described above. To set the defaults, just click the icon in the upper left of the command prompt and select "Defaults."

Future plans

For those wondering why I launch karma over and over rather than just leaving it running, I'm planning an entire post on karma setup in the future.

Deja vu all over again

english mobile

When I started this blog in 2007, I had been developing in a technology called Authorware that Adobe was in the process of killing off. So I decided to concentrate on teaching myself Flex, sharing what I was learning with others who were also making that transition.

I still work in Actionscript a lot, primarily in Flash, but unfortunately I've had to come to the decision that it's not the best use of my time to blog about some of the neat things I'm still doing.

It's not fair to say I'm learning JavaScript, since I've been doing JavaScript in some form for over ten years, but I am learning about the JavaScript ecosystem as I find it in 2014, particularly surrounding AngularJS. I'm shifting focus to once again ramp up in tools that are relevant instead of ones that are becoming less relevant. In the process, hopefully I can help others who are making a similar transition or just who want to learn about the parts of the process that I delve into. I like to think that I'm willing to write about things that others may not feel are glamorous enough to devote time to but that can be incredibly helpful if you're looking for just that information.

I have decided to keep the title of the blog the same. In part this is because I have some loyal readers who have been with me the whole way. In part this is because I think that the AngularJS world will realize in time that the particular experience that former Actionscript developers, especially Flex developers, bring to the table means that we're very well-suited to do things the "Angular way." I hope you enjoy the new direction.