Introduction

Unit Testing

what is unit testing?

why should we unit test?

what tools do we use to unit test?

why do we need both karma and jasmine?

Karma Config

module.exports = function (config) {

  config.set({

    // base path that will be used to resolve all patterns (eg. files, exclude)
    basePath: '',

    // frameworks to use
    // available frameworks: https://npmjs.org/browse/keyword/karma-adapter
    frameworks: [
      'jasmine',
      'phantomjs-shim'
    ],

    // test results reporter to use
    // possible values: 'dots', 'progress'
    // available reporters: https://npmjs.org/browse/keyword/karma-reporter
    reporters: ['progress', 'coverage'],

    // optionally, configure the reporter
    coverageReporter: {
      type: 'html',
      dir: 'coverage'
    },

    // preprocess matching files before serving them to the browser
    // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
    preprocessors: {
      'app/views/**/*.html': 'ng-html2js',
      'app/js/**/*.js': 'coverage'
    },

    junitReporter: {
      outputFile: 'junit/test-results.xml',
      suite: ''
    },

    // list of files / patterns to load in the browser
    files: [

      // bower library
      'app/bower_components/angular/angular.js',
      'app/bower_components/ui-router/release/angular-ui-router.js',
      'app/bower_components/angular-mocks/angular-mocks.js',

      // our app
      'app/js/**/*.js',

      // tests
      'tests/unit-tests/**/*.js',

      //location of templates
      'app/views/**/*.html'

    ],

    // list of files to exclude
    exclude: [],

    // web server port
    port: 9876,

    // enable / disable colors in the output (reporters and logs)
    colors: true,

    // level of logging
    // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
    logLevel: config.LOG_INFO,

    // enable / disable watching file and executing tests whenever any file changes
    autoWatch: false,

    // start these browsers
    // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
    browsers: ['PhantomJS'],

    // Continuous Integration mode
    // if true, Karma captures browsers, runs the tests and exits
    singleRun: true

  });
  
};

Some Terms

suite - a javascript function containing more suits or specs, this is represented with describe() method, the signature looks like this:

describe('in this factory', function() {
  
});

Some Terms

spec - has similar signature to describe, it is represented by it() method and this is where you will make your assertions. The goal is for the tests to be human readable: ie: "it should do something"

it('should return correct sum', function() {
   
});

Some Terms

  • assertions - are functions provided by Jasmine that allow you to assert or assume a function will provide the expected results.
     
  • idempotent - your test's shouldn't affect the next. Use teardown
     

  • stub/mock/spy - focus on testing ONE function and mocking dependencies.

Jasmine Assertions

  • toEqual() - only checks the value
  • toBe() - compares objects, could be equal but not the same
  • toBeDefined() - is not undefined
  • toContain() - useful for searching within a string
  • toHaveBeenCalled() - if has passed through the expected function
  • not() - can be chained, will return you the opposite, ie: not.toBe()

Suite & Spec Example

var numberSquared = function(a) {
  return a * a;
};

describe('math computations', function() {
  it('should return the passed in number squared', function() {
    expect(numberSquared(2)).toEqual(4);
    expect(numberSquared(3)).toEqual(9);
    expect(numberSquared(4)).toEqual(16);
  });
});

Some More Terms

focusing - you can focus a suite or a spec by putting f in front: fit() and fdescribe(), now when you run karma it will only test the focused suites and specs (used to be iit and ddescribe)

ignoring - the opposite of what focusing does, putting an x in font: xit() and xdescribe()

Some More Terms

afterEach - used to teardown your spec, so it doesn't effect the next test, will return literally after every spec

beforeEach - very frequently used, will run before every spec

What Should We Test?

  • first test your services/factories/providers because these contain logical functions that can be easily tested and offer more value
     
  • assuming you have most of your logic in services, then you shouldn't have to test your controllers, but if you don't then you should consider moving it to a service
     
  • testing directives is tricky because you're not only testing javascript, you're also testing DOM manipulation associated with the directive

Factory Example

describe('mathFactory', function () {
 
  var mathFactory;
 
  beforeEach(module('demoApp'));
 
  beforeEach(inject(function ($injector) {
 
    mathFactory = $injector.get('mathFactory');
 
  }));
 
  it('should have convertMinutesToHours method defined', function () {
 
    expect(mathFactory.convertMinutesToHours).toBeDefined();
 
  });
 
  it('should return hours formatted correctly', function () {
 
    expect(mathFactory.convertMinutesToHours(1)).toBe('0:01');
    expect(mathFactory.convertMinutesToHours(122)).toBe('2:02');
    expect(mathFactory.convertMinutesToHours(489)).toBe('8:09');
 
 
  });
 
});

Controller Example

describe('homeCtrl', function () {
 
  var $scope,
    $controller;
 
  beforeEach(module('demoApp'));
 
  beforeEach(inject(function ($injector) {
 
    $scope = $injector.get('$rootScope').$new();
    $controller = $injector.get('$controller');
    $controller('homeCtrl', { $scope: $scope });
 
  }));
 
  it('should have $scope variables defined', function () {
    expect($scope.hello).toBeDefined();
    expect($scope.items).toBeDefined();
    expect($scope.addItem).toBeDefined();
  });
 
  it('should increase the length of items array when calling addItem', function () {
 
    expect($scope.items.length).toEqual(0);
    $scope.addItem('unit testing')
    expect($scope.items.length).toEqual(1);
 
  });
 
});

Directive Example

describe('hello directive', function () {

  var $rootScope, $compile, markup, element, $directiveScope;

  beforeEach(module('demoApp'));

  beforeEach(inject(function ($injector) {

    $rootScope = $injector.get('$rootScope');
    $rootScope.madeUpName = 'Ace Ventura';
    $compile = $injector.get('$compile');
    markup = '<hello-world name="{{madeUpName}}"></hello-world>';
    element = $compile(markup)($rootScope);
    $directiveScope = element.isolateScope();
    $rootScope.$digest();

  }));

  it('should have proper dom structure with the correct name', function () {

    // less specific
    expect(element.html()).toContain('Ace Ventura')

    // super specific
    expect(element.html()).toEqual('<h3 class="ng-binding">Hello my name is Ace Ventura</h3>');

  });


});

Bad Practices

  • be careful with false positives
     
  • functions that make $http requests, all tests can NOT be async
     
  • do not create large test suites, break it up into scenarios
     
  • you don't have to run all the tests when developing, focus on the test you're working on (fit, fdescribe)
     
  • don't try to get too fancy, don't try to run tests in a loop or test randomness

Good Practices

  • make smoke screen tests that will run first, if those don't pass then there's no need to run anything else, this is the basic test that should always pass
     
  • organize your tests into folders and name the folders in order you want them to run, i.e. smoke screen first
     
  • code coverage is a guideline but not the law
     
  • integrate your repo with your bamboo build, on every commit run unit test to give you the most up to date status
     
  • do not push with focused tests

UTE-UI

  • we currently have 110 tests
  • roughly 50% coverage
  • running on 3 browsers

Unit Testing

By Chester Rivas

Unit Testing

Presentation on unit testing in the AngularJS environment

  • 1,579