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

Friday, June 10, 2016

More Filter Testing

I recently wrote a post on how to test whether a filter was called from a controller.  Today I needed to test whether a filter was called from a factory, which is slightly different.  When we test controllers we instantiate the controller and inject what we want.  That means when we inject the $filter service we can just supply our own spy instead.  In a factory, though, we don't really instantiate the factory.  Instead the factory is just kinda there and we inject other factories and services into it.  (I'm sure there's a way to "instantiate" a factory for testing purposes and I know you don't actually "instantiate" things in JavaScript, but get over it.)  I needed a way to globally tell Angular that during testing I didn't want to use the normal $filter service when it came across it in my factory.  Fortunately, I also recently wrote a post on how to test the $mdSidenav service that's part of Angular Material. I put the two posts together and came up with a solution that works very well.

What I ultimately did was checked into the Angular source code for the $filter service (here) and found that it's pretty straightforward.  It just uses the $injector service to find the registered filter by name and return it.  So I mimicked that in my spy, except that I returned another spy when I came across the filter I wanted to test.  It sounds a bit confusing (even to me) writing it out so why don't you just check out the code below.  That should make more sense.

The code:
angular.module('app', []).factory('myFactory', function($filter) {
  var factory = {};

  factory.states = [
    { name: 'Alabama', id: 'AL' },
    { name: 'Alaskas', id: 'AK' },
    { name: 'Arizona', id: 'AZ' },
    { name: 'Arkansas', id: 'AR' }
  ];
  
  factory.sort = function() {
    return $filter('orderBy')(factory.states, 'id');
  };

  return factory;
});

The spec:
describe('test suite', function() {
  var myFactory, orderByFilterSpy, filterSpy;
  
  beforeEach(module('app'));
  
  beforeEach(module(function($provide, $injector){
    orderByFilterSpy = jasmine.createSpy('orderBy');
    filterSpy = jasmine.createSpy('$filter').and.callFake(function(name) {
      switch(name) {
        case 'orderBy':
          return orderByFilterSpy;
        default:
          return $injector.get(name + 'Filter');
      }
    });

    $provide.factory('$filter', function() {
      return filterSpy;
    });
  }));

  beforeEach(inject(function(_myFactory_) {
    myFactory = _myFactory_;
  });
  
  it('should call orderByFilter', function() {
    // arrange
    myFactory.states = [{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}];

    // act
    myFactory.sort();

    // assert
    expect(filterSpy).toHaveBeenCalledWith('orderBy');
    expect(orderByFilterSpy).toHaveBeenCalledWith([{id: 'AZ', name: 'Arizona'}, {id: 'AL', name: 'Alabama'}, {id: 'AK', name: 'Alaska'}, {id: 'AR', name: 'Arkansas'}], 'id');
  });
});

That's it!  As a little added bonus, any filters other than orderBy that happen to get called in my factory should get passed through (I haven't validated that part yet, but it looks like it would do that so I'm rolling with it).

Friday, June 3, 2016

Testing $mdMedia

As I mentioned before I'm working with Angular Material on my project.  Today I had cause to use the $mdMedia service, which accepts a string parameter and returns either true or false based on the screen size.  For example, $mdMedia('lg') will return true if the screen is between 1280px and 1919px.  The service is great, but testing it was - once again - tricky.

I ended up using the same trick I used to test $mdSidenav, but modified it just a little bit.  In the interests of making it easier on myself next time I have to test $mdMedia, here we go.
var mediaMock, mediaQueryResult;
beforeEach(module(function ($provide) {
  mediaMock = jasmine.createSpy('$mdMedia');
  $provide.factory('$mdMedia', function() {
    return function() {
      return mediaQueryResult;
    };
  });
}));

it('should do something when $mdMedia() returns false regardless of what is passed to it', function() {
  // arrange
  mediaQueryResult = false;

  // act
  myFactory.myFunction();

  // assert
  expect(myFactory.myOtherFunction).toHaveBeenCalled();
});

it('should do something when $mdMedia() returns true regardless of what is passed to it', function() {
  // arrange
  mediaQueryResult = true;

  // act
  myFactory.myFunction();

  // assert
  expect(myFactory.myOtherFunction).not.toHaveBeenCalled();
});

That's all I had to do to get it to work.  I'm sure there's a way to vary the result based on the parameter, but I didn't need to do that so I didn't solve that problem... yet.

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');
    });
});

Friday, February 19, 2016

Matching Regular Expressions with $httpBackend

Let's say you're doing unit testing like a good little developer and you're using Jasmine for it.  Let's further say you're testing Angular and you want to ensure that the correct URL is called on your service, but you don't necessarily care what parameters are passed, just that they're named correctly.  With the $httpBackend service you can match an endpoint using a regular expression so you don't have to worry about the actual values of the parameters.

Here's what I mean:
$httpBackend.whenGET(/Person\/GetById\?personId=.*&userId=.*&/).respond(200, []);

Any request that goes to [anything]Person/GetById?personId=[anything]&userId=[anything] will match and the $httpBackend service will return a 200 response with an empty array.

Works like a charm.

Monday, June 23, 2014

More Angular .NET Routing

This is a quickie to expand on a couple of things I mentioned here.

In that other post I mentioned that you'll need a controller for each page if you want your users to be able to navigate to them directly. That's true, but you don't need a separate view for each one.  What I do instead is redirect every action method for every controller to the same view (~/Views/Home/Index).  That way the Angular routes can take over and everything will work the way you want without a bunch of clutter in your solution.












The next thing is a bit of a correction, sort of.  In the last post I said you should replace @RenderBody() with something else.  Well, I had to create my application anew and that little bit really caused me some trouble.  It now appears you don't want to remove @RenderBody() at all because that will cause some issues in your site.  I'm not issuing a formal change until I can get back to that original application (on my other computer) and try to figure out why it works there and not here.  In the meantime, you can do this instead of following step 22.






There's bound to be more little things I find so I'll try to keep things updated as much as possible.

Wednesday, June 18, 2014

Angular.NET Routing

Well, hello again.  In this post I'm going to explain how to set up routing with Angular routes to implement a Single Page Application (SPA).  A SPA may not be best for your needs, but I believe it's helpful to know how to do something (or at least that it's possible) before you need to actually do it.

First things first, if you haven't read the first part of this post, you may want to do that before you continue.  Even if you're already familiar with AngularJS and .NET, I created a sample application in that post that I'll be expanding in this post.

One really important note before we jump into this: if you want people to be able to hit each page directly in a SPA, you'll still need to create .NET controllers.  For example, we're going to route everyone who hits http://localhost:47125 to Pages/Home.html, but if we don't have a .NET controller for /Home/Index the user will get a 404 error.  I don't cover that part in this guide because it's late and I want to go read my book before I go to bed.  Don't laugh... seriously, I can still hear you.  What, a guy can't read before bed?

  1. In the last post, we only added AngularJS Core from NuGet.  This time we're going to add the full blown Angular suite.  Follow steps 4 - 7 in the last post, but when you get to step 8, choose "Angular JS" instead of "AngularJS Core"
    1. When you do this, you should get a couple of warnings that the file already exists; that's OK, go ahead and replace the current file with the new one
  2. Open the file you created in step 16 of the last post, "angular-start.js"
  3. Inside the brackets add "'ngRoute'" (with the single quotes around it, but not the double quotes)
  4. Beneath that, add what you see below
  5. There is something very important to note here: I'm not routing to "Views/Home.cshtml" or "Home[Controller]/Index".  We're going to create a Pages directory and use that, otherwise (in my experience at least) your .NET routes will interfere with your Angular routes
  6. Right-click "AngularTutorial.Presentation.Web" and choose "Add > New Folder" from the popup menu
  7. Name the folder "Pages"
  8. Right-click the new "Pages" folder and choose "Add > New Item..." from the popup menu
  9. Scroll down the list of items you can add and click "HTML Page"
  10. Change the name to "Home.html"
  11. Click the "Add" button
  12. Repeat steps 8 - 11 of this tutorial twice, once for "Contact.html" and once for "About.html"
  13. Follow steps 28 - 33 in the first tutorial three times, creating "contactController.js", "aboutController.js", and "homeController.js"
  14. Copy/paste the contents of "angularController.js" into each of the three files you created in step 13
  15. Change the name of the controller in each of the files you created in step 13 to match the name of the file
    1. For example, change "angularController" to "homeController" in the homeController.js file
  16. Change the message to something unique for each controller
  17. Expand "AngularTutorial.Presentation.Web > App_Start"
  18. Open "BundleConfig.cs"
  19. In the bundle you created in step 37 of the first post, add the three new controllers you created in step 13 of this post
  20. In the same bundle, add the "angular-route.js" file that should have been included in your project in step 1
  21. Expand "AngularTutorial.Presentation.Web > Views > Shared" and open "_Layout.cshtml"
  22. Replace @RenderBody() with this
  23. In the same file, locate the ul with id "menu" and remove the 3 default tags
  24. Insert these tags where you just removed those others
  25. Notice the path for each of them corresponds to the routes you created in step 4
  26. Follow steps 40 - 43 of the first post

Monday, June 16, 2014

Angular.NET

OK, so the title is a bit misleading.  There is no such thing as Angular.NET (as far as I know), but that doesn't mean we can't mash up AngularJS with ASP.NET to make some really neat applications.  I had a bit of trouble getting started and I couldn't find a comprehensive tutorial anywhere so I put one together.  I even used pictures!

This tutorial is intended to walk you through every tiny step so it may seem tedious at times.  Just go with it.  Or scroll past the parts you already know and get to the good stuff.  I just wanted to be really thorough so next time I start something from scratch I don't have to wonder at all about what I did last time to make it work.

There are a couple of items to note before we jump in.  First (and possibly most importantly), all file names are arbitrary.  If I say "Create a file called angular-start.js" and you want to call it "friggafrompton.js", that's fine.  But later when I reference "angular-start.js", you'll have to remember you named it "friggafrompton.js" and make the appropriate adjustments.  Secondly, the reason I'm starting with a blank solution is for future enhancements to this application.  If you want to just create a new project, that's fine, but again, you're responsible for remembering you did that later.  Next, when you see a word inside brackets, it references a key on the keyboard. So [Enter] means the Enter key on your keyboard.  When you see something in quotes and I tell you to type it somewhere, don't type the quotes.


  1. Open Visual Studio (I use VS 2012)
  2. Create a new Blank Solution named "AngularTutorial"
  3. Add a project named "AngularTutorial.Presentation.Web" to the blank solution
  4. Expand "AngularTutorial.Presentation.Web" and right-click "References"
  5. Click "Manage NuGet Packages"
  6. In the left windowpane, select "nuget.org" or "All"
  7. In the upper right-hand corner of the window, type "AngularJS" and press [Enter]
  8. For this tutorial, only install AngularJS Core
  9. Expand "AngularTutorial.Presentation.Web > Views > Shared"
  10. Open _Layout.cshtml
  11. Edit the html tag in _Layout.cshtml to include "ng-app='angularApp'"
  12. Expand "AngularTutorial.Presentation.Web"
  13. Right-click "Scripts"
  14. Choose "Add > New Item..."
  15. Scroll down the list of items you can add and click "Javascript File"
  16. Change the name to "angular-start.js"
  17. Click the "Add" button
  18. Add the following line to the top of the blank file ("angular-start.js")
  19. Expand "AngularTutorial.Presentation.Web"
  20. Right-click "Controllers"
  21. Choose "Add > Controller..."
  22. Name the Controller "AngularController"
  23. Click the "Add" button
  24. In the open "AngularController.cs" file, right-click "View()" and choose "Add View..."
  25. Check the box marked "Create as a partial view"
  26. Click the "Add" button
  27. Add the following markup to the top of the blank file ("Index.cshtml")
  28. Expand "AngularTutorial.Presentation.Web"
  29. Right-click "Scripts"
  30. Choose "Add > New Item..."
  31. Scroll down the list of items you can add and click "Javascript File"
  32. Change the name to "angularController.js"
  33. Click the "Add" button
  34. Add the following line to the top of the blank file ("angularController.js")
  35. Expand "AngularTutorial.Presentation.Web > App_Start"
  36. Open "BundleConfig.cs"
  37. Somewhere in the RegisterBundles method, add the following code
  38. Go back to "_Layout.cshtml"
  39. After the jQuery bundle is added, add the new angular bundle
  40. Build the solution
  41. Start without debugging
  42. Navigate to /Angular
    1. In my case it was http://localhost:47125/Angular
  43. Witness the fruits of your labor