Search code examples
mocha.jsappceleratorappcelerator-titaniumappcelerator-alloyappcelerator-studio

How to write test cases for titanium with alloy


I am stuck while writing test cases for alloy framework as i am not getting how to use controllers and alloy files in mocha testing framework. I searched on google and few links suggested below code to moke a controller but it throws error that "TypeError: alloy.createController is not a function".

var alloy = require('../../alloy'); it('Verify row controller', function() {

        console.log(JSON.stringify(alloy))
        var controller = alloy.createController('login', {
            name : "uniqueName",
        });

        // if(controller.passwordTest.value !== "uniqueName"){
        // throw new ("Verify row controller FAILED");
        // }
    });

Solution

  • Currently, I can show you a (slightly modified) example of our codebase.

    First of all, our controller tests are pure javascript tests. All calls to the Ti api are executed against a mock. We solely focus on the controller under test and all dependencies are mocked.

    We rely on jasmine and jasmine-npm for that.

    • install jasmine-npm; see https://github.com/jasmine/jasmine-npm
    • create 'spec' folder in the root of your project (folder with tiapp.xml)
    • place all your test files inside this folder
    • the filenames of test files must end with _spec.js
    • run the jasmine command from within the root folder of your project

      describe('authenticate controller test', function() {

      var USER_NAME = "John Doe";
      
      var fooControllerMock = {
          getView: function(){}
      };
      
      var fooViewMock = {
          open: function(){}
      }
      
      
      Ti = {
          // create here a mock for all Ti* functions and properties you invoke in your controller
      }
      
      Alloy = {
          CFG: {
              timeout: 100
          },
          Globals: {
              loading: {
                  hide: function(){}
              },
              networkClient: {
                  hasAutoLogin: function(){}
              },
              notifications: {
                  showError: function(){}
              }
          },
          createController: function(){}
      };
      
      var controllerUnderTest; // class under test
      
      $ = {
          btnAuthenticate: {
              addEventListener: function(){}
          },
          authenticate: {
              addEventListener: function(){},
              close: function(){}
          },
          username: {
              addEventListener: function(){},
              getValue: function(){}
          },
          password: {
              addEventListener: function(){}
          },
          windowContainer: {
              addEventListener: function(){}
          }
      };
      
      L = function(s){
          return s;
      };
      
      beforeEach(function () {
          controllerUnderTest = require('../app/controllers/auth');
      });
      
      
      
      it('should create foo controller when authentication was succesful', function(){
          spyOn(Alloy.Globals.loading, 'hide');
          spyOn(Alloy, 'createController').and.returnValue(fooControllerMock);
          spyOn(fooControllerMock, 'getView').and.returnValue(fooViewMock);
          spyOn($.username, 'getValue').and.returnValue(USER_NAME);
          spyOn($.auth, 'close');
      
          controllerUnderTest.test._onAuthSuccess();
      
          expect(Alloy.Globals.loading.hide).toHaveBeenCalled();
          expect(Alloy.createController).toHaveBeenCalledWith('foo');
          expect(fooControllerMock.getView).toHaveBeenCalled();
          expect($.auth.close).toHaveBeenCalled();
      });
      
      it('should show error message when a login error has occured', function(){
          spyOn(Alloy.Globals.loading, 'hide');
          spyOn(Alloy.Globals.notifications, 'showError');
      
          controllerUnderTest.test._onAuthLoginError();
      
          expect(Alloy.Globals.loading.hide).toHaveBeenCalled();
          expect(Alloy.Globals.notifications.showError).toHaveBeenCalledWith('msg.auth.failure');
      });
      
      });
      

    Be sure to write a mock implementation(just empty) for all Ti* stuff you call from within your controller. I know this is quite cumbersome but a solution is on it's way.

    Note that we already created an npm package which has a generated mock for this (based on api.jsca), together with some code with which you can mock all required dependencies, together with a set of testing best practices. However we will validate this code internally before we opensource it. I hope we can write our blog post and expose our accompanying github repo within a few weeks. Just keep an eye on tiSlack.

    Controller code:

    function _onAuthSuccess() {
        Alloy.Globals.loading.hide();
        Alloy.createController('foo').getView().open();
        $.authenticate.close();
    }
    
    function _onAuthLoginError() {
        Alloy.Globals.loading.hide();
        Alloy.Globals.notifications.showError(L('msg.auth.failure'));
    }
    
    function _onAuthTokenValidationFailure() {
        Alloy.Globals.loading.hide();
    }
    
    function _authenticate() {
        var username = $.username.value;
        var password = $.password.value;
    
        if(Alloy.Globals.validationEmail.isValidEmailAddress(username)){
            Alloy.Globals.loading.show(L('authenticate.msg.logging.in'), false);
        } else {
            Alloy.Globals.notifications.showError(L('app.error.invalid.email'));
        }
    }
    
    function _onNetworkAbsent() {
        Alloy.Globals.loading.hide();
        Alloy.Globals.notifications.showError(L('global.no.network.connection.available'));
    }
    
    function _hideKeyboard() {
        $.username.blur();
        $.password.blur();
    }
    
    function _focusPassword() {
        $.username.blur();
        $.password.focus();
    }
    
    function _init() {
        Ti.App.addEventListener('auth:success', _onAuthSuccess);
        Ti.App.addEventListener('auth:loginFailed', _onAuthLoginError);
        Ti.App.addEventListener('app:parseError', _onAppParseError);
        Ti.App.addEventListener('network:none', _onNetworkAbsent);
    
        $.btnAuthenticate.addEventListener('click', ..);
        $.authenticate.addEventListener('close', _cleanup);
        $.username.addEventListener('return', _focusPassword);
        $.password.addEventListener('return', _authenticate);
    
        $.windowContainer.addEventListener('touchstart', _hideKeyboard);
    }
    
    _init();
    
    function _cleanup() {
        Ti.API.info('Closing and destroying the auth controller');
         ... 
        $.windowContainer.removeEventListener('touchstart', _hideKeyboard);
    
        $.destroy();
        $.off();
        }
    
        module.exports = {
        test: {
            _onAuthSuccess: _onAuthSuccess,
            _onAuthLoginError: _onAuthLoginError
            }
        }
    

    and the corresponding view:

    <Alloy>
        <Window>
            <View id="windowContainer">
                <TextField id="username" />
                <TextField id="password" >
                <Button id="btnAuthenticate" />               
            </View>
        </Window>
    </Alloy>