A backend-less plunker

January 7, 2015    plunker

So you are asking for help and someone says… hey plunk it so we can see it.

Right, even when we should be able to reproduce any problem in a small example, there are some difficult ones.

Imagine this conversation:

  • Hey I am creating a todo list but when I try to update a todo it changes and then immediately changes back.
  • Good, can you make a plunker for that?
  • Uhm no, it uses my (insert here a backend) and I can’t use it on a plunker.

It is not an uncommon issue at all. What could be a good solution? Swap our service that talks with the backend with a different one swapping the $http calls with fake data? Yeah, we could, but if you start modifying your original implementation, it will be harder to debug, because the problem could lie on that original service and it is not there to test.

Ok… so what to do?

First, we are going to plunk what we have:

angular.module('plunker', [])

  .factory('Todos', function($http) {
    return {
      all: function() {
        return $http.get('/api/todos');
      }
    };
  })

  .controller('MainCtrl', function($scope, Todos) {
    Todos.all().then(function(result) {
      $scope.todos = result.data;
    });
  });

And:

<h2>Our list of Todos</h2>
<ul>
  <li ng-repeat="todo in todos">
    <input type="checkbox" ng-model="todo.completed" />{{ todo.title }}
  </li>
</ul>

So we get nothing, well lies, we get a GET http://run.plnkr.co/api/todos 404 (Not Found) in the console. That was expected, the $http service is trying to reach and non existent route to fetch our data.

So again, what can we do?

Angular folks created the framework with testability in mind and we can certainly use that in our advantage here.

The $http service which is also used by $resource and restangular, does not talk with your backend directly, in fact there is another layer called $httpBackend which is the one that does all the real stuff. We can use that layer to create a fake backend for our example.

The first thing we need to do, is to add angular-mocks.js as a dependency in our plunker. Angular mocks will swap the original $httpBackend with a fake one that we can use for testing or to simulate a backend. Oh, simulate a backend, just what we need.

Before we proceed, there is something important to keep in mind: When we swap the $httpBackend, it will swallow every $http request.

Right, we create a backend.js file on our plunker (and its script tag after the app.js file) so we can start coding our fake backend:

angular.module('plunker')
  .run(function($httpBackend) {
    
  });

So on application start, we will add our backend logic starting with some fake data:

var things = [
  {
    id: 0,
    title: 'Finish fake backend',
    completed: true
  },
  {
    id: 1,
    title: 'Make some cool stuff',
    completed: false
  },
  {
    id: 2,
    title: 'Brainstorm new projects',
    completed: false
  }
];

Good, we have a couple of things for our todo list so now we need a way to GET them:

$httpBackend.whenGET('/api/todos').respond(200, things);

The mocked $httpBackend provides a couple of whenXXX methods that we can use to catch all those requests and process them.

In this case we are using whenGET which receives an URL as parameter (which is the URL of the request) and then we use the .respond method on it where we pass the status code and the data we want to send back.

This can be read like: When we do a GET on /api/todos please respond with a status 200 (OK) and this list of todo items.

For a starter, that should be enough for our little demo, isn’t it? Sure, try it out…

Nah, it doesn’t work yet, how so? We are still working with the original $httpBackend because we haven’t told Angular to use the Angular mocks one. We can do that in two ways:

  • We can add ngMockE2E as a dependency on our plunker module.
  • Or we can swap the $httpBackend manually with a decorator.

The first approach is easier, but the second one is better because we can have all our fake backend stuff in one file so we can plug it in any plunker easily.

Alright, how can we do that? Just add a .config method inside backend.js like this:

.config(function($provide) {
  $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
})

With that we are doing something like… get the $httpBackend service and change it for this one from Angular mocks.

If we try our example now… it works!

So, to recap here: We have our original implementation angular implementation and also we have a pluggable fake backend which we can just use in any plunker without hassle or extra configuration.

Still a backend with just support GET requests is not a backend, so let’s finish the backend.js implementation.

POST requests:

$httpBackend.whenPOST('/api/todos').respond(function(method, url, data, headers) {
  var newItem = JSON.parse(data);
  newItem.id = things.length;
  things.push(newItem);
  
  return [201, newItem];
});

We can use a callback function in .respond which receives:

  • method -> POST in our case.
  • url -> /api/todos here.
  • data -> The object we send with the POST.
  • headers -> The headers of the request.

For a POST request, we parse our data (a new todo), we assign it an id, we push it to our list of things and finally we return an array composed of the status code and the new item (like a real backend would do).

PUT request:

$httpBackend.whenPUT(/^\/api\/todos\/\d+$/).respond(function(method, url, data, headers) {
  var item = JSON.parse(data);
  for (var i = 0, l = things.length; i < l; i++) {
    if (things[i].id === item.id) {
      things[i] = item;
      break;
    }
  }
  
  return [200, item];
});

A PUT request needs a parameter which in our case is the todo to update. You could expect us to use /api/todos/:id as the endpoint, but that is a syntactic sugar that we don’t have here. So instead of that, we will use a regexp /^\/api\/todos\/\d+$/ which will basically match a PUT request done to /api/todos/X where the X is a number.

Alright, now we have our PUT endpoint and all we need to do is to parse the data which contains the updated fields and then search the corresponding todo to update it. We could use the parameter to find the correct todo, but it this case we have the id on the item as well. Finally as always we return an array with the status code and the updated item.

DELETE request:

$httpBackend.whenDELETE(/^\/api\/todos\/\d+$/).respond(function(method, url, data, headers) {
  var regex = /^\/api\/todos\/(\d+)/g;
  
  var id = regex.exec(url)[1]; // First match on the second item.
  id = parseInt(id, 10);
  for (var i = 0, l = things.length; i < l; i++) {
    if (things[i].id === id) {
      var index = things.indexOf(things[i]);
      things.splice(index, 1);
      break;
    }
  }
  
  return [204];
});

The difference here compared to the PUT one is that we don’t pass any data with the id so we need to grab it from the URL and then find the correct todo to delete it. My convention on delete is to just return a 204 code (Everything OK but nothing get returned). You can easily grab the item before deleting it and return it as well.

With this we have our complete backend that we can simply drop where needed (it is not tied to plunker).

Still, there is something left we need to resolve. Remember when I said that this $httpBackend is going to swallow all requests? When we set a templateUrl, that is going to use $http to get the template and that is going to be swallowed as well, so we can simply add another rule for that:

$httpBackend.whenGET(/\.html/).passThrough();

When we do a GET to something ending with .html we let the request do the real thing. That will allow plunker to use external templates.

So, our backend.js file will end like:

angular.module('plunker')

  .config(function($provide) {
    $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator);
  })
  .run(function($httpBackend) {
    var things = [
    {
      id: 0,
      title: 'Finish fake backend',
      completed: true
    },
    {
      id: 1,
      title: 'Make some cool stuff',
      completed: false
    },
    {
      id: 2,
      title: 'Brainstorm new projects',
      completed: false
    }
  ];
  
  $httpBackend.whenGET('/api/todos').respond(200, things);
  
  $httpBackend.whenPOST('/api/todos').respond(function(method, url, data, headers) {
    var newItem = JSON.parse(data);
    newItem.id = things.length;
    things.push(newItem);
    
    return [201, newItem];
  });
  
  $httpBackend.whenPUT(/^\/api\/todos\/\d+$/).respond(function(method, url, data, headers) {
    var item = JSON.parse(data);
    for (var i = 0, l = things.length; i < l; i++) {
      if (things[i].id === item.id) {
        things[i] = item;
        break;
      }
    }
    
    return [200, item];
  });
  
  $httpBackend.whenDELETE(/^\/api\/todos\/\d+$/).respond(function(method, url, data, headers) {
    var regex = /^\/api\/todos\/(\d+)/g;
    
    var id = regex.exec(url)[1]; // First match on the second item.
    id = parseInt(id, 10);
    for (var i = 0, l = things.length; i < l; i++) {
      if (things[i].id === id) {
        var index = things.indexOf(things[i]);
        things.splice(index, 1);
        break;
      }
    }
    
    return [204];
  });
  
  $httpBackend.whenGET(/\.html/).passThrough();
});

As a final example, let’s use the Angular example of todomvc in a plunker and then plug our fake backend:

comments powered by Disqus