0

We just started implementing jasmine tests in our AngularJS project and I have a question:

We want to test this controller function:

$scope.deleteClick = function () {
        $scope.processing = true;
        peopleNotesSrv.deleteNote($scope.currentOperator.operatorId, $scope.noteId, $scope.deleteSuccessCallback, $scope.deleteErrorCallback);
    };

We wrote this following test:

    it('deleteClick should pass proper parameters to peopleNoteSrv', function () {
        $controllerConstructor('PeopleNoteEditCtrl', { $scope: $scope });

        $scope.noteId = 5;

        expect(function () { $scope.deleteClick(); }).not.toThrow();
    });

This test makes sure that when we call the $scope.deleteClick() function that $scope.processing is set to true and that the call to peopleNotesSrv doesn't throw any errors because of invalid arguments. We are testing the two callback functions in separate tests.

Should we be testing that the peopleNotesSrv.deleteNote function was called so the test is more explicit? The way this test is written right now it doesn't really tell someone what the deleteClick() function does under the hood and that seems to be incorrect.

nweg
  • 2,825
  • 3
  • 22
  • 30
  • Yes, you could mock out `peopleNotesSrv` service and spy on deletNote method... – PSL Oct 14 '14 at 15:34

2 Answers2

1

Ask yourself what you'd do if you had developed it using TDD. It pretty much goes the direction Sam pointed out, but here are some examples:

Controller Tests

  1. start writing a test which would expect a deleteClick to exist.
  2. Expect deleteClick to setup the loading state (check for processing = true)
  3. Test whether a service is injected into the controller (peopleNotesSrv)
  4. Check whether deleteClick calls the service (as already mentioned via spies)
  5. Verify that $scope.noteId and the other $scope.params are present and set

This is as far as it relates to the Controller. All the criteria whether it fails or throws errors etc. should be tested in a Service.spec. Since I don't know your service in detail here some examples

Service Tests

  1. Ensure deleteNote exists
  2. Check what happens if wrong number of arguments (less or more) are supplied
  3. Make some positive tests (like your noteId = 5)
  4. Make some negative tests
  5. Ensure callbacks are properly called

... and so on.

Testing for validity in controllers doesn't make a lot of sense because than you'd need to do it for every Controller you have out there. By isolating the Service as a separate Unit of Test and ensure that it fulfills all the requirements you can just use it without testing. It's kinda the same as you never would test jQuery features or in case of Angular jQLite, since you simply expect them to do what they should :)

EDIT:

Make controller tests fail on service call

Pretty easy lets take this example. First we create our Service Test to ensure that the call fails if not the proper number of arguments is supplied:

describe('Service: peopleNoteSrv', function () {

  // load the service's module
 beforeEach(module('angularControllerServicecallApp'));

 // instantiate service
 var peopleNoteSrv;
 beforeEach(inject(function (_peopleNoteSrv_) {
   peopleNoteSrv = _peopleNoteSrv_;
 }));

 it('should throw error on false number of arguments', function () {
   expect(function() { peopleNoteSrv.deleteNote('justOneParameter'); }).toThrow();
 });

});

Now to ensure that the test passes lets create the error throwing part in our service method

angular.module('angularControllerServicecallApp')
  .service('peopleNoteSrv', function peopleNoteSrv() {

    this.deleteNote = function(param1, param2, param3) {
      if(arguments.length !== 3)
        throw Error('Invalid number of arguments supplied');
      return "OK";
    };
});

Now lets create 2 demo controllers, FirstCtrl will do it properly, but SecondCtrl should fail

angular.module('angularControllerServicecallApp')
  .controller('FirstCtrl', function ($scope, peopleNoteSrv) {
    $scope.doIt = function() {
      return peopleNoteSrv.deleteNote('param1', 'param2', 'param3');
    }
  });

angular.module('angularControllerServicecallApp')
  .controller('SecondCtrl', function ($scope, peopleNoteSrv) {
    $scope.doIt = function() {
      return peopleNoteSrv.deleteNote('onlyOneParameter');
    }
  });

And both controller as a demo have following test:

it('should call Service properly', function () {
  expect(scope.doIt()).toBe("OK");
});

Karma now spits out something like this:

Error: Invalid number of arguments supplied
    at [PATH]/app/scripts/services/peoplenotesrv.js:15
    at [PATH]/app/scripts/controllers/second.js:13
    at [PATH]/test/spec/controllers/second.js:20

Thus you exactly know that you missed to update SecondCtrl. Of course this should work for any of your tests consuming the Service method.

Hope that's what you meant.

zewa666
  • 2,593
  • 17
  • 20
  • we are primarily concerned about catching errors when a signature changes on a service. Let's say we add a new argument to the service call. We want all the controller tests to fail that are now calling that service without the proper arguments. How would you go about doing that? – nweg Oct 14 '14 at 19:36
  • OK I've updated my previous answer, hope this is what you meant – zewa666 Oct 14 '14 at 20:35
0

I think the answer is that it depends.

There are two cases:


1 - You also have a suite of tests for the peopleNotesSrv service.

In this case I would leave this test as-is or check a few more things around the specific functionality of $scope.deleteClick(), such as if there are any watchers on $scope.processing that do anything specific regarding a .deleteClick() call.


2 - You do not have any tests for all the possible functionality for the peopleNotesSrv service.

In this case I would write a more explicit test that does check that the .deleteNote() actually performed it's job.


In my opinion you should really build tests up and try to not test the same thing in more than one place, as this adds extra work and could produce holes in the tests if you think, "Well I can just test this specific case when it gets called from a specific function that calls it."

What if you ever want to reuse that deletNote() as part of a bigger function in a different place?Then you need to write another test for the same code because it is being called from a different function.

So I would aim for case 1, this way you can write all your tests for that service and then trust that those tests cover the rest of this particular test. If you throw errors on bad input or for failures to actually delete a note, you should trust that other code to test what it was designed to test. This will greatly speed up your test-writing time and increase the chance that your tests cover all the cases. It also keeps all the tests for that service in the same place in your test code.

I think also a good question to start with is what kind of test is this? Unit Test or End-to-End test?

I was assuming it was a Unit Test for my answer, if it was an End-to-End test, then you might want to keep following the function calls to verify everything is happening as you expect.

Here are some links on Unit Tests, End-to-End tests, and a pretty good article about both and Angular.

Community
  • 1
  • 1
Sam
  • 118
  • 6