Testing

The Technical Bits

Ionic Testing Tutorials

Angular Hybrid Testing Tools

AngularJS Testing Tutorials

Other Unit Testing Frameworks

Common Concepts

  • Tests are independent, may run in any order or even in parallel
  • Put common setup actions, e.g., connect to test DB, in a setup method
  • Put common cleanup actions in a tear down method

Common Formats

  • Object-oriented (JUnit-inspired):
    • tests inherit assertmethods from a TestCase class
    • superclass handles collecting all methods labeled as tests, running them, counting successes and failures, and reporting results
  • Behavior-Driven:
    • "it should ..." sentences label each test rather than method names
    • May have Given (preconditions), When (initial actions), and Then (expected results) structure

Behavior-Driven Development

My first “Aha!” moment occurred as I was being shown agiledox by Chris Stevenson. It takes a JUnit test class and prints out the method names as plain sentences, so:

public class CustomerLookupTest extends TestCase {
    testFindsCustomerById() {
        ...
    }
    testFailsForDuplicateCustomers() {
        ...
    }
    ...
}

becomes

CustomerLookup
- finds customer by id
- fails for duplicate customers
- ...

When [developers] wrote the method name in the language of the business domain, the generated documents made sense to business users, analysts, and testers.

Naming Test Methods

  • Pattern: testEventCorrectResult
    • BAD testAccounts
    • BAD testDeposit
    • BAD testDepositZero
    • OK testDepositZeroIsError
    • OK testDepositZeroLeavesBalanceUnchanged
  • Side benefit: encourages one test to a test

Describing Tests

  • Many frameworks, including Jasmine, use descriptive texts rather than function names
describe('sorting the list of users', function() {
  it('sorts in descending order by default', function() {
    var users = ['jack', 'igor', 'jeff'];
    var sorted = sortUsers(users);
    expect(sorted).toEqual(['jeff', 'jack', 'igor']);
  });
});
https://docs.angularjs.org/guide/unit-testing

What Not to Test

  • Private functions
  • Trivial functions like setters and getters
  • Logging functions like toString

Unit Tests: The Challenge

  • Unit tests
    • should be numerous, fast, automated
    • should test only the unit, not other classes
    • should not cross module boundaries
  • So how can you unit test code that
    • calls code in other modules
    • is slow, e.g., opening a database connections
    • calls code that doesn't exist yet

Solution: Mock Objects

  • Mocks let you test that code calls another object correctly and uses the results correctly
    • A mock is a spy that records calls to it
    • A mock is a stub that imitates an object's responses
  • Mock libraries provide tools for making mocks in just a few steps
  • Mocks are also very useful for "Plan B" demos!!

Readings: Mock Objects

Mock Objects (Other)

JS Code to Unit Test

(function() {
  angular.module('app').controller('LoginController', ['$state', '$ionicPopup', 'DinnerService', LoginController]);

  function LoginController($state, $ionicPopup, dinnerService) {
    
    var vm = this;
    vm.doLogin = function () {

      var onSuccess = function () {
          $state.go('my-dinners');    
      };

      var onError = function () {
          $ionicPopup.alert({
               title: 'Login failed :(',
               template: 'Please try again.'
             });
    };

    dinnerService.login(vm.username, vm.password)
                 .then(onSuccess, onError);
  }

})();

JS Unit Test

describe('LoginController', function() {
  ...
  describe('#doLogin', function() {
    ...
    it('should call login on dinnerService', function() {
      expect(dinnerServiceMock.login).toHaveBeenCalledWith('test1', 'password1'); 
    });
    
      describe('when the login is executed,', function() {
      it('if successful, should change state to my-dinners', function() {
        ...
        expect(stateMock.go).toHaveBeenCalledWith('my-dinners');
      });
      
      it('if unsuccessful, should show a popup', function() {
        ...        
        expect(ionicPopupMock.alert).toHaveBeenCalled();
      });
    });
  })
});

JS Unit Tests Set Up

describe('LoginController', function() {
  ...
  describe('#doLogin', function() {
  
    // call doLogin on the controller for every test
    beforeEach(inject(function(_$rootScope_) {
      $rootScope = _$rootScope_;
      controller.username = 'test1';
      controller.password = 'password1';
      controller.doLogin();
    }));
       
    it('should call login on dinnerService', function() {
    ...

JS Defining Mocks

describe('LoginController', function() {

  var controller, deferredLogin, dinnerServiceMock, stateMock, ionicPopupMock;
  ...
  // instantiate the controller and mocks for every test
  beforeEach(inject(function($controller, $q) {
    deferredLogin = $q.defer();
    
    // mock dinnerService
    dinnerServiceMock = {
      login: jasmine.createSpy('login spy')
              .and.returnValue(deferredLogin.promise)     
    };
    ...    
  }));
  
  describe('#doLogin', function() {
  ...

JS Spying On Methods

describe('LoginController', function() {
            ...
  // instantiate the controller and mocks for every test
  beforeEach(inject(function($controller, $q) {
    ...
    // mock $state
    stateMock = jasmine.createSpyObj('$state spy', ['go']);
    
    // mock $ionicPopup
    ionicPopupMock = jasmine.createSpyObj('$ionicPopup spy', ['alert']);
    
    // instantiate LoginController
    controller = $controller('LoginController', { 
            '$ionicPopup': ionicPopupMock, 
            '$state': stateMock, 
            'DinnerService': dinnerServiceMock }
           );
  }));
  
  describe('#doLogin', function() {
  ...

Step 1: Prepare for Mocking

If language needs it, create interfaces for classes to be mocked

public class Warehouse {
  public int getInventory(int unitId) {
   ...db query...
  }
  ...
}

becomes

public interface Warehouse {
  public int getInventory(int unitId)
  ...
}

public class WarehouseImpl implements Warehouse {
  public int getInventory(int unitId) {
   ...db query...
  }
  ...
}

This is better code, even if mocking is not needed.

Step 2: Replace Objects with Mocks

Using the JMOCK library, a normal JUnit test

public class OrderTester extends TestCase {
  private Warehouse warehouse = new WarehouseImpl();
  ...
  public void testOrderIsFilledIfEnoughInWarehouse() {
    Order order = new Order(TALISKER, 50);
    order.fill(warehouse);
    assertTrue(order.isFilled());
    assertEquals(0, warehouse.getInventory(TALISKER));
  }

becomes

public class OrderTester extends MockObjectTestCase {
  ...
  public void testOrderIsFilledIfEnoughInWarehouse() {
    Order order = new Order(TALISKER, 50);
    Mock warehouseMock = new Mock(Warehouse.class);
    ...
    order.fill((Warehouse) warehouseMock.proxy());
    assertTrue(order.isFilled());
    warehouseMock.verify();
  }

Step 3: Define Mocks

Using the JMOCK library

public void testOrderIsFilledIfEnoughInWarehouse() {
    Order order = new Order(TALISKER, 50);
    Mock warehouseMock = new Mock(Warehouse.class);
 
    
warehouseMock.expects(once()).method("hasInventory") .with(eq(TALISKER),eq(50)) .will(returnValue(true)); warehouseMock.expects(once()).method("remove") .with(eq(TALISKER), eq(50)) .after("hasInventory");
order.fill((Warehouse) warehouseMock.proxy()); warehouseMock.verify(); assertTrue(order.isFilled()); }

Mocking for Rapid Native App Development