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.

Avatar for William Rijksen

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