Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Making sure your metal doesn’t break

Making sure your metal doesn’t break

An introduction into Titanium Alloy model/collection testing to get you started using Mocha, Proxyquire, Sinon and other tooling. Presented at 14th of December (2016) at meetup.com/TitaniumNL.

Link to example app: https://github.com/williamrijksen/TYM-talk.

William Rijksen

December 14, 2016
Tweet

Other Decks in Technology

Transcript

  1. WHY SHOULD YOU TEST YOUR BUSINESS LOGIC? Less coffee Don’t

    wait for build Test input-output Checks your code Find out code breaks By new features
  2. EXAMPLE UNIT TEST // example.test.js import assert from 'assert'; describe('example',

    function() { it('1 is equal to 2', function() { assert.equal(1, 2); }); }); FAILS example 1) 1 is equal to 2 0 passing (171ms) 1 failing SUCCEEDS example ✓ 1 is equal to 1 1 passing (235ms) // example.test.js import assert from 'assert'; describe('example', function() { it('1 is equal to 1', function() { assert.equal(1, 1); }); });
  3. GOAL: BUILD A BANKAPPLICATION 1. Bank balance Test Alloy collections

    2. Validate transaction Test validation Alloy model 3. Bank-transfer Test NFC Handling
  4. DEPENDENCIES ! Mocha - Testing framework ! Backbone - Extends

    Alloy models/collections ! Proxyquire - Proxies a require() ! Sinon - Record callbacks
  5. CREATE ALLOY MODEL exports.definition = { config: { // columns

    and such }, extendModel: function(Model) { return Model; }, extendCollection: function(Collection) { return Collection; } }; BACKBONE COLLECTION EXTENDED BY ALLOY BACKBONE MODEL EXTENDED BY ALLOY TRANSACTION MODEL
  6. STUB ALLOY MODEL var _ = require('alloy/underscore'); exports.definition = {

    extendModel: function(Model) { _.extend(Model.prototype, { // define functions }); return Model; } }; MODEL NEEDS PROXYQUIRE ADDED AFTER ALLOY TRANSPILING
  7. STUB ALLOY MODEL var _ = require('alloy/underscore'); exports.definition = {

    extendModel: function(Model) { _.extend(Model.prototype, { // define functions }); return Model; } }; const getModule = name => ( proxyquire(`../app/models/${name}`, { 'alloy/underscore': underscore }) ); const getModel = name => ( getModule(name).definition.extendModel( Backbone.Model.extend({}) ) ); export { getModel, }; MODEL TEST UTILS
  8. MOCK BACKBONE COLLECTION const getModule = name => ( proxyquire(`../app/models/${name}`,

    { 'alloy/underscore': underscore }) ); const getModel = name => ( getModule(name).definition.extendModel( Backbone.Model.extend({}) ) ); const getCollection = name => ( getModule(name).definition.extendCollection( Backbone.Collection.extend({ model: getModel(name), }) ) ); export { getCollection }; extendCollection: function(Collection) { _.extend(Collection.prototype, { // define functions }); return Collection; } MODEL TEST UTILS var _ = require('alloy/underscore'); exports.definition = { ... // extend model };
  9. TEST BALANCE OF TRANSACTIONS transaction collection 1) getBalance is 0

    with zero transactions 0 passing (124ms) 1 failing RUNTIME ERROR FUNCTION NOT EXISTS STUBBED COLLECTION import assert from 'assert'; import { getCollection } from '../../testutils'; const TransactionCollection = getCollection('transaction'), items = []; // Test: getBalance is 0 with zero transactions const collection = new TransactionCollection(items); assert.equal(collection.getBalance(), 0);
  10. MAKE TEST PASSING // Test: getBalance is 0 with zero

    transactions const collection = new TransactionCollection([]); assert.equal(collection.getBalance(), 0); extendCollection: function(Collection) { _.extend(Collection.prototype, { }); return Collection; } transaction collection ✓ getBalance is 0 with zero transactions 1 passing (99ms) TEST MODEL getBalance: function() { return 0; }
  11. ADD MORE TESTS transaction collection ✓ getBalance is 0 with

    zero transactions 1) getBalance is -100 with one depreciation of 1 euro 1 passing (130ms) 1 failing ASSERTION ERROR // Test 2: buy something and check getBalance const collection = new TransactionCollection([{ ..., amount: -100, }]); assert.equal(collection.getBalance(), -100);
  12. FIX ASSERTION ERROR _.extend(Collection.prototype, { getBalance: function() { var sum

    = 0; _.each(this.models, function(item) { sum += item.get('amount'); }); return sum; } }); transaction collection ✓ getBalance is 0 with zero transactions ✓ getBalance is -100 with one depreciation of 1 euro ✓ getBalance is 100 with a salary of 1 euro ✓ getBalance is 500 with a salary of 10 euros and one with one depreciation of 5 euros EXPAND CALCULATION
  13. VALIDATE MODEL extendModel: function(Model) { _.extend(Model.prototype, { validate: function(attrs) {

    var errors = []; return errors.length > 0 ? errors : false; } }); return Model; } VALIDATE BEFORE SAVE MODEL
  14. TEST VALIDATE ON SAVE const TransactionModel = getModel('transaction'); // Test:

    amount may not be a string const invalidCallback = sinon.spy(), model = new TransactionModel(); model.bind('invalid', invalidCallback); TEST STUBBED MODEL VALIDATES ON SAVE EVENT
  15. TEST VALIDATE ON SAVE const TransactionModel = getModel('transaction'); // Test:

    amount may not be a string const invalidCallback = sinon.spy(), model = new TransactionModel(); model.bind('invalid', invalidCallback); TEST model.save({ ..., amount: '10', }); sinon.assert.calledWith( invalidCallback, model, [{ name: 'amount', message: 'Amount must be a number' }] ); validate model 1) amount may not be a string 0 passing (130ms) 1 failing 1) validate model amount may not be a string: Error: A "url" property or function must be specified RUNTIME ERROR
  16. FIX RUNTIME-ERROR Error: A "url" property or function must be

    specified validate model 1) amount may not be a string 1) validate model amount may not be a string: AssertionError: false == true RUNTIME ERROR ASSERTION ERROR const getModel = name => ( getModule(name).definition.extendModel(backbone.Model.extend({ sync: function sync(method, model, options) { // prevent sync sends request to the internet options.success(this, this.toJSON(), options); model.trigger('sync', model, this.toJSON(), options); }, })) ); SYNC ALWAYS SUCCESS
  17. FIX ASSERTION ERROR extendModel: function(Model) { _.extend(Model.prototype, { validate: function(attrs)

    { var errors = []; if (!_.isNumber(attrs.amount)) { errors.push({ name: 'amount', message: 'Amount must be a number' }); } return errors.length > 0 ? errors : false; } }); return Model; } validate model ✓ amount may not be a string 1 passing (128ms) MODEL
  18. transaction model validate ✓ no attr is empty ✓ amount

    may not be 0 ✓ amount may not be a string validates iban on save ✓ triggers invalid event for wrong iban ✓ triggers success event for correct transaction transaction collection ✓ getBalance is 0 with zero transactions ✓ getBalance is -100 with one depreciation of 1 euro ✓ getBalance is 100 with a salary of 1 euro ✓ getBalance is 500 with a salary of 10 euros and one with one depreciation of 5 euros 9 passing (192ms) ADD MORE TESTS
  19. NFC HANDLER var nfcModule = require('ti.nfc'); module.exports = function() {

    var nfcAdapter, self = this; _.extend(this, Backbone.Events); function discovered(e) { self.trigger('discovered', e); } nfcAdapter = nfcModule.createNfcAdapter({ onNdefDiscovered: discovered }); }; TRIGGERED ON NFC NATIVE MODULE TRIGGER BACKBONE EVENT
  20. TEST NFC HANDLER TRIGGER ON NFC STUB TI.NFC // Test:

    trigger discovered on NFC-discover const nfcAdapter = {}, const tiNfc = { createNfcAdapter: args => (_.extend(nfcAdapter, args)), }; const NFCHandler = proxyquire('../../../app/lib/NFCHandler', { 'ti.nfc': tiNfc, ..., }); const nfcHandler = new NFCHandler(); const discoverHandler = sinon.spy(); // Listen to backbone event nfcHandler.on('discovered', discoverHandler); nfcAdapter.onNdefDiscovered(); sinon.assert.calledOnce(discoverHandler);
  21. NFC TRANSACTION var NFCHandler = require('./NFCHandler'); module.exports = function (transactionCollection)

    { }; MAKE PAYMENT ON NFC DISCOVER TRIGGER ON NFC CREATE AND ADD TO COLLECTION var nfcHandler = new NFCHandler(); function newTransaction() { transactionCollection.create({ name: 'Test payment', iban: 'BE68539007547034', amount: -500, description: 'Payment for Maikel' }); } nfcHandler.on('discovered', newTransaction);
  22. NFC TRANSACTION ... // stub NFCHandler, NFCTransactionLib // Test: discovered

    nfc tag creates a new transaction const nfcHandler = new NFCHandler(); const NFCTransaction = getNFCTransactionLib(nfcHandler); const transactionCollection = new Backbone.Collection([]); const nfcTransation = new NFCTransaction(transactionCollection); nfcHandler.trigger('discovered'); assert.equal(transactionCollection.length, 1); TEST PAYMENT HAS BEEN MADE ON DISCOVER NFCTransaction ✓ discovered nfc tag creates a new transaction