Unit testing Angular the right way



There are a number of different methods used to test code, we’ll discuss integration testing and unit testing.

A unit test is a test which is written to verify the functionality of a relatively small piece of code. They are intended to be narrow in scope and cover what the programmer considers useful.

With Angular, there is no excuse for not testing.

https://docs.angularjs.org/guide/unit-testing

With unit tests, methods and services external to the function being directly tested are typically mocked out so the test never hits an external system or function.

Alternatively, integration tests help to demonstrate that various parts of a system work together. They may require external resources, such as databases or services, or at a minimum may require several functions within the codebase to produce a valid result. They are different than unit tests because they can access external resources or functions to validate a use case.

Integration tests tend to be more difficult to set up, but can demonstrate that the system works as a whole.

Integration tests can be written against a UI element (in a website or application). Additionally, command-line applications and RESTful applications can have integration tests.

Unit vs Integration

Unit tests and integration tests are not exclusive. You can have both sets of tests in your project, however there may be a more appropriate time to write and run a unit test vs an integration test.

Unit tests are the first test you want to write. They can be written before developing your code in a TDD (test driven development) fashion, or after you’ve written your function to “lock in” your API.

Unit tests are also the first test you want to run. They can be run dynamically during development using file watchers, or on save. They are always the first test to run when your project builds as well, as they can catch coding errors and point to the precise location of a problem more quickly than integration tests.

In web development, integration tests are usually developed by a QA team or developer using a system called Protractor along with Karma. You could also use Jasmine to access elements and exercise button or link clicks, but this tends to be more difficult to write. They are run when your project builds and are typically the second type of test to run, after your unit tests.

An integration test will access an element and/or click an element, then test the result to determine if the element followed the use case requirements written in the ticket (if you’re using a ticketing system).

Integration tests can use mocked data or live data from the services, however they typically always use the real UI.

In a more lenient definition of integration tests, one can also execute a function directly, which may interact with several other functions or services. The integration concept of this test is that the function executed is part of an integrated system of functions and APIs, and the output of the function under test matches the expected behavior. This type of test shouldn’t be considered a unit test because unit tests should never execute outside functions.

http://stackoverflow.com/questions/5357601/whats-the-difference-between-unit-tests-and-integration-tests

“Unit testing limits refactoring”

FALSE

In my experience, unit testing actually helps a developer to refactor code more confidently.

First, a developer can use the unit test to understand the code. They can change the code and see if the test breaks or continues to work.

Developers can go beyond a single function change and refactor entire sections of the website, easily removing individual unit tests which correspond to removed functions in the code. If another unit test breaks outside of the code being modified then that will point out links between your code under change and the rest of the system, giving you the ability to easily refactor those functions as well.

If you have integration tests as well you can run those to check that your code changes have not negatively affected the UI.

When is a Unit test Appropriate?

Unit tests should be the first type of test created by a programmer. You can write it before you write any code, or you can write your code and then follow it with a unit test which “locks in” your code.

An integration test can be defined when you have a view created and have programmed that view to react to certain use cases. It’s more difficult to define an integration test before your view is created since you wouldn’t know what elements to target. Integration tests will test your adherence to the business requirements and use cases.

  • Did you just create a function? — Unit test
  • Does that function touch other services or functions? — Unit test, mock out external dependencies
  • Did you create a UI element that shows or hides based on certain circumstances? — Unit test, then Integration test
  • Does that element execute a function to show or hide? — Unit test that function

Unit Test Structure

Try it out on your own! Fork the Codepen and try to create your own unit tests for the provided directive.

Use the code below and the linked Codepen as examples for unit testing.

describe 'InvoicesController', -> # The thing that we're testing
  # Global variables that will be used later
  $rootScope = null
  $scope = null
  $controller = null
  giPaginatorService = null
  $httpBackend = null

  # The module you need to inject
  module('AccountApp')

  # Use underscores in the inject so that you can reassign the variables
  # ALWAYS use the same variable name. Don't use "service" for "giPaginatorService" for example.
  beforeEach inject (_$controller_, _$rootScope_, _giPaginatorService_, _$httpBackend_) ->
    # Assign underscore variables to ones with the same name, sans underscore
    $controller = _$controller_
    $rootScope = _$rootScope_
    $scope = $rootScope.$new()
    giPaginatorService = _giPaginatorService_
    $httpBackend = _$httpBackend_;

    # Spy on global service calls before instantiating the controller
    spyOn(giPaginatorService, 'setItemsPerPage')
    spyOn(giPaginatorService, 'setItemsTotalCount')
    spyOn(giPaginatorService, 'setPage')

    # Instantiate the controller and inject any mocks, like $scope
    controller = $controller('InvoicesController', {
      $scope: $scope
    })

  # Each function/method in the controller gets its own describe block one-level nested
  describe 'init method', ->
    beforeEach ->
      # If there are more than 1 it's and they need the same setup, then use a beforeEach
      # Mock out shared functions here
      spyOn($scope, 'getStatusFilterString').and.returnValue('test')

    # Each 'it' describes a code path or use case for the function being tested.
    # Code paths are essentially if/else blocks. Each if should have 2 tests, 
    #   but some leeway is given if there's no else block or an implied else.
    it 'initializes the scope variables', ->
      # Spy on functions that aren't used in other places here
      # You can also mock out data here
      spyOn($scope, 'loadData')
      # Then execute the function being tested
      $scope.init()
      # Then write your expects
      expect(giPaginatorService.setItemsPerPage).toHaveBeenCalledWith(15)
      expect($scope.sortBy).toEqual("orderId")

    it 'initializes the scope variables with fake', ->
      # You can also rewrite a previously defined spy 
      $scope.getStatusFilterString.and.returnValue('fake')
      
      $scope.init()
    
      expect(giPaginatorService.setItemsPerPage).toHaveBeenCalledWith(15)
      expect($scope.sortBy).toEqual("orderId")

When should I use angular.element() in a Unit Test?

NEVER!

In no unit test should you ever see angular.element() or a JQuery selector. Those selectors are reserved for integration tests.

If you happen to see an Angular controller or service selecting an element from the DOM, then that controller or service needs to be rewritten. The only thing which should modify the DOM is a directive’s template. In rare instances a directive itself can modify the DOM, but these are rare cases. Most directives are dumb wrappers on top of services.

What are the benefits to writing unit tests?

  • Better code coverage (minimum 80% rule)
  • Easier tests to write
  • Don’t need to understand every nuance of the system to write a unit test
  • Improves understanding of code and how it works together
  • Reduction of private functions, better testability (all functions under test must be exposed for testing)
  • Allows the developer to double-check their logic
  • Exposes bugs during code and unit test writing
  • Exposes bugs during code build/deployment
  • Exposes pesky date and time bugs
  • Faster to create than integration tests
  • Faster to run than integration tests

General tips

  • Write one it per code path (if/else path).
  • Don’t be afraid to write multiple expects inside one it.
  • Common mistake: don’t write one it per expect, this slows down the test process.
  • Use a code coverage tool or webpage to identify areas that need testing.
  • Use a similar structure for all tests, this makes it easy to find examples and write new tests.
  • Be sure to mock or spy on every external resource. If you didn’t write it, you don’t need to test it.
  • Don’t test third-party tools. For instance, we don’t need to test whether Angular’s ng-repeat works. If you didn’t write it, you don’t need to test it.

Speed Comparison

The examples below originally used an integration testing style, where the page is loaded into Tealeaf (the testing framework used) and elements are examined in the page using JQuery or Angular selectors. Functions in the angular code are typically tested using click events.

The new code uses the methods defined above, where we load up the directive or controller and only test the Angular code. The elements on the page aren’t touched for the most part because they’re controlled by the model in the Angular code, so any change in the page would be a result of the model changing.

ContactInfoController Spec

Old New Results
Finished in 0.25500 seconds Finished in 0.02600 seconds 9.8x faster
2 examples, 0 failures 4 examples, 0 failures 2 more examples
61.9% Statements 13/21 100% Statements 21/21
33.33% Branches 1/3 100% Branches 3/3
80% Functions 4/5 100% Functions 5/5
61.9% Lines 13/21 100% Lines 21/21 38.1% more lines tested

Invoices Spec

Old New Results
Finished in 0.60200 seconds Finished in 0.10600 seconds 5.68x faster
6 examples, 0 failures 25 examples, 0 failures 19 more examples
80.49% Statements 132/164 86.59% Statements 142/164
67.74% Branches 21/31 77.42% Branches 24/31
73.47% Functions 36/49 79.59% Functions 39/49
80.49% Lines 132/164 86.59% Lines 142/164 6.1% more lines tested

PurchaseHistoryController Spec

Old New Results
Finished in 1.42300 seconds Finished in 0.17100 seconds 8x faster
20 examples, 0 failures 53 examples, 0 failures 33 more examples
65.54% Statements 116/177 85.31% Statements 151/177
54.65% Branches 47/86 74.42% Branches 64/86
62.79% Functions 27/43 81.4% Functions 35/43
65.54% Lines 116/177 85.31% Lines 151/177 19.77% more lines tested

Leave a Reply

Your email address will not be published. Required fields are marked *