Tuesday, May 31, 2016

Testing Whether A Filter Was Called

Today I finally circled back on some old tests and decided to figure out how to tell whether a specific filter was called with the correct values.  This has been bothering me for a while, but the answer turned out to be pretty simple.  Let's say you have a controller that calls the built-in orderBy filter and passes an array of states (scope.states) to be sorted by id:
angular.module('app', []).controller('sampleController', function($scope, $filter) {
  $scope.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  $scope.sort = function() {
    return $filter('orderBy')(scope.states, 'id');
  };
});

The end result should be that these states get sorted so Alaska comes first, followed by Alabama, Arkansas, then Arizona (AK, AL, AR, AZ).  In order to verify this works as expected we can inject a new spy into the controller instead of using the expected $filter service.  Like this:
describe('test suite', function() {
  var scope, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(inject(function(_$controller_, _$rootScope_) {
    scope = _$rootScope_.$new();
    
    orderByFilterSpy = jasmine.createSpy();
    filterSpy = jasmine.createSpy().and.returnValue(orderByFilterSpy);
    
    _$controller_('sampleController', { $scope: scope, $filter: filterSpy });
  }));
  
  it('should pass scope.states and \'id\' to orderByFilter', function() {
    scope.sort();
    
    expect(filterSpy).toHaveBeenCalled();
    expect(orderByFilterSpy).toHaveBeenCalledWith([
      {name: 'Alabama', id: 'AL'},
      { name: 'Alaskas', id: 'AK' },
      { name: 'Arizona', id: 'AZ' },
      { name: 'Arkansas', id: 'AR' }], 'id');
  });
});

What we end up with is one passing test.  Remember to trust that the orderBy filter does what you're expecting.  That means you're not actually testing whether the result of calling scope.sort is a sorted array.  Instead you just check that you passed the right values to the right filter.  If you're calling a custom filter, make sure to test that filter thoroughly, but separately.

Tuesday, May 24, 2016

Testing $mdSidenav

I'm working with Angular Material on my current project and it's been interesting.  I'm writing unit tests to try to cover every line, branch, etc. (at least every reasonable combination) and I came across a situation where I needed to confirm that $mdSidenav was being called properly.  I'm using Jasmine and Karma and I couldn't get it to work.

Fortunately, I wasn't the only person who ran into this problem and I found part of my answer here.  That allowed me to verify that $mdSidenav() was called, but not what value was passed to it.  I had to add one more little piece and I was good to go.  Here's what I did.

My controller function:
$scope.openSidenav = function() {
    $mdSidenav('menu').toggle();
};

And my test:
it('openSidenav should pass \'menu\' to $mdSidenav.toggle', function() {
    $controller('toolbarController', { $scope: scope, hotkeys: hotkeys });
 
    scope.openSidenav();
 
    expect(sideNavToggleMock).toHaveBeenCalled();
    expect(passedSideNavId).toBe('menu');
});

But the most important part is in the setup of the tests. After I create the module (like this:
beforeEach(module('quotelite'));) I have to create a spy assigned to a global variable, then use the $provide service to register a factory with the name $mdSidenav and set it to... you know what? Here's the code:
beforeEach(module(function ($provide) {
    sideNavToggleMock = jasmine.createSpy('$mdSidenav');
    $provide.factory('$mdSidenav', function() {
        return function(sideNavId) {
            passedSideNavId = sideNavId;
            return {
                toggle: sideNavToggleMock
            };
        };
    });
}));

That allows me to check everything that needs to be checked.  I'll be honest when I say that I'm not 100% certain how that's working, but I know that it works and I'll figure out the "how" part later.  For completeness, here's my full spec:
describe('myController', function() {
    var $controller, scope, sideNavToggleMock, passedSideNavId;
 
    beforeEach(module('app'));

    beforeEach(module(function ($provide) {
        sideNavToggleMock = jasmine.createSpy('$mdSidenav');
        $provide.factory('$mdSidenav', function() {
            return function(sideNavId) {
                passedSideNavId = sideNavId;
                return {
                    toggle: sideNavToggleMock
                };
            };
        });
    }));
 
    beforeEach(inject(function(_$controller_, _$rootScope_){
        scope = _$rootScope_.$new();
        _$controller_('myController', { $scope: scope });
    }));
 
    it('openSidenav should pass \'menu\' to $mdSidenav.toggle', function() { 
        scope.openSidenav();
  
        expect(sideNavToggleMock).toHaveBeenCalled();
        expect(passedSideNavId).toBe('menu');
    });
});

Wednesday, May 18, 2016

File Upload with Angular

HTML5 offers some cool features for uploading files, including the ability to drag and drop a file from your computer onto an area of the web page and have that file uploaded.  But Angular doesn't come with a built-in way to do it.  Fortunately (and this is one of the reasons I love Angular so much) there's already a community-built, open-source directive available for just that.  Actually, there are a lot of them, but I picked one in particular and it's working pretty well so far.  The one I picked is called ng-file-upload and can be found here.

That's great and all, but it didn't quite get me all the way to where I needed to be.  For that I needed another post, found here.  In that post the author explains how to structure the request with a FormData object.  It boils down to this:
var fd = new FormData();
fd.append('file', file);
$http.post(uploadUrl, fd, {
    transformRequest: angular.identity,
    headers: {'Content-Type': undefined}
});
You can put that in the watch (though you should really abstract it into a service) on 'file' (or whatever your scope variable is that's bound to ng-model of ng-file-upload) and you'll be able to upload the file correctly.  I had to add a few other things, like usingangular.toJson(item)to stringify an object as part of the parameter to the server, but that's specific to my implementation. The above code should be enough to have a drag and drop feature that does what you need in a basic use case.

Tuesday, May 17, 2016

Using Batch Files to Make Things Easier

Right now I'm working purely on the front end of the project I'm on, so I'm not using an IDE for the most part.  Instead I'm running everything through npm, which makes significant use of the command prompt.  That means that every time I want to start debugging again I have to launch three separate command prompts (one for lite-server, one for gulp, and one for karma), issue a cd command to the correct directory (which is nested pretty deep), then issue my actual command (npm start, gulp watch, and npm test, respectively).  I got tired of doing that over and over so I wrote a batch file to do it for me.
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm start"
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && gulp watch"
start cmd.exe /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm test"
As an added bonus, I can use the /min switch to minimize the windows as soon as they're opened.
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm start"
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && gulp watch"
start cmd.exe /min /k "cd c:\dev\secondLevel\thirdLevel\fourthLevel\fifthLevel && npm test"

Thursday, May 5, 2016

Smarter Spying in Jasmine

While I was testing some Angular code today I came across a section that was a bit difficult to test.  There's a try/catch block where the try calls a function (numeral.language) and then the catch calls the same block.  The issue with testing was that if I spied on numeral.language and used .and.throwError() the function would throw an error both times and my test would 'splode.  The workaround turned out to be pretty straightforward.


var callCount;
beforeEach(function() {
  callCount = 0;
  spyOn(numeral, 'language').and.callFake(function() {
    if (callCount === 0) {
      callCount++;
      throw new Error('Unknown language');
    }
  });
});

The first time through the method (when callCount is 0), manually throw an error (instead of using .and.throwError()) and increment callCount.  The next time the fake gets called, it won't throw an exception.  I'm still working on how to call through to the original method after that, but I don't need it right now and I didn't want to forget this.