Introducing Best Practices such as Unit Tests and TypeScript to a Legacy Project built with a Pre ES6 Framework such as ExtJS

There are instances when Legacy JS projects are actively developed which are based off of   Pre-ES6 era frameworks such as ExtJS or even just plain vanilla ES5 or earlier. But that’s no excuse for not writing automated tests. There may not be an appetite for reaching optimal code coverage numbers for the entire code base, but at a minimum a developer must not deliver  new or modified code without accompanying Unit tests. It is always ideal to improve the  coverage on an existing code base if the project is in active development for any reason.

Delivery of Source code without accompanying automated tests to assure quality is incomplete

Introducing unit testing to a legacy project based on frameworks such as ExtJS (Sencha) or vanilla JS plus Typescript can improve  quality and speed up the development velocity. Typescript is a superscript of JavaScript. It can be safely introduced in to most JS projects without having to refactor existing code.

Benefits of introducing TypeScript to a JS project

  • Discover  typing errors during the Transpilation phase
    • Detect incorrect usages of objects and methods, non-conformance of method signatures and improper use of variable types.
  • Speed up the development by leveraging IDE code complete features or predict the possible usages of a variable, method or an Object due to the introduction of ‘Types’.

Tools needed to setup and run the tests

  • A tool that can bootstrap the Unit Tests. Karma & Ava fit this bill. Most projects use a Build/Bundler tool such as Webpack or Gulprun for various build tasks such as
    • Run the Typescript compiler for transpilation,
    • Invoke the execution of Unit Tests through  Karma or directly through Jasmine,
    • Convert SASS to CSS
    • Minify the sources and pretty much prepare the sources as needed for deployment.
  • A framework such as Jasmine to write the actual tests
  • A Continuous Integration system (Jenkins/Bamboo) for automatically running the build tasks and for optionally notifying the team on test failures  and for promoting builds. The goal is to fail the build when tests fail.

Steps

Basic Project setup

Assuming that you already have a code base, Just go to the root of your code base and run any commands listed below

  • Install Node.js
  • Install Yarn (This is quite superior and blazingly when fast compared to npm)
  • Initialize your project through Yarn by running “yarn init”
  • Add the dependencies (karma, jasmine & typescript)
    • yarn add package_name –dev
  • Install dependencies by simply running “yarn” at the root of the folder.

Setup Karma configuration

  • Create a folder (test) than can hold the  test cases and the test configuration
  • In the “test” folder, create a configuration file for karma titled “karma.conf.js
  • The order of the array in the “files” section is important. Here’s the proper order for this setup
    • Load any core frameworks being used (ExtJS in this instance)
    • Load any Test Libraries (Sometimes certain parts of the sources may be mocked. An example could be a Login module or a shared module that is needed by the application and you need fine grained controls so you prefer to create a test version of it instead of plainly mocking it using Jasmine)
    • Load the application sources
    • Load the test cases (specs)

Setup Typescript configuration

  • Create a tsconfig.json in the “test” folder. This can actually be moved to the root if typescript needs to be introduced to the entire project instead of just the test cases.

Write your tests (specs)

Let’s first talk about the application that needs to be tested. Clone this repository and browse to the “app/view” folder

  • This is a simple CRUD application for store real names of our favorite Super Heroes. (Run the index.html from the root of the application in the browser to check it out)
  • The HeroGrid.js is the primary view and holds the main Grid that displays an existing list of Super Hero names
  • It has a form in it’s header through which new entries can be added.
  • The Grid is backed by HeroGridController.js, (Controller) HeroGridModel.js (View Model), and  Hero.js (Model)

Testing the Grid Configuration

The Grid itself simply extends a standard ExtJS grid and  holds configuration (both display and component configuration) and declaration of the Controller method names that need to be triggered based on appropriate user actions.

At first glance, it may  appear that there isn’t much to test. But testing the display  and column configuration or even the title can be handy in certain situations. Here’s a simple spec for the same.

  • The describe() method takes two arguments. A description of the test suite and a function which includes the actual tests.
  • The actual tests are invoked by calling the it() method. The first param describes the behavior of the test case (Hence the term “Behavior Driven Driven” (BDD) tests attribution given to the tests written with Jasmine)
  • The beforeEach() method  is invoked every time before running. In our case we just re-initialize the this.cmp so that each test case has a fresh untouched instance  of the component to test.
  • “Ext.isDomReady = true” forces Ext to think that the DOM has been loaded in the Unit testing environment for it to work properly.
describe("Tests for Hero Grid Component", function(){
    let cmp
    beforeEach(function () {
        Ext.isDomReady = true
        this.cmp = Ext.create('SuperHeroes.view.HeroGrid')
    })

    it("Component height should be a fixed 500", function(){
        expect(this.cmp.height).toEqual(500)
    })

     it("Component title should be 'Super Heroes Grid'", function(){
        expect(this.cmp.title).toEqual("Super Heroes Grid")
    })

    it("Grid's first column must be the first name", function(){
        expect(this.cmp.columns[0].dataIndex).toEqual("first_name")
    })
})

Testing the Controller

The meat of the CRUD implementation is in  HeroGridController.js through the onAdd(), onUpdate() and onDelete() methods. Review the spec for the controller here.

Let’s break this down and go through the key elements of the spec.

The beforeEach() method

Since we need to test the Controller, Why not just create an instance of the controller instead of creating an instance of the component and extracting the controller from it? This is because the controller relies on it’s component, viewModel and other dependencies. Instead of manually setting up all of these dependencies, creating an instance of a component sets up all of these dependencies for us and the necessary wiring between them. So the component that is extracted this way is ready for testing.

beforeEach(function () {
    Ext.isDomReady = true
    this.record = Ext.create('SuperHeroes.model.Hero', {
        id: 1, first_name: "John", last_name: "Doe"
    })
    this.cmp = Ext.create('SuperHeroes.view.HeroGrid')
    this.ctrl = this.cmp.getController()

     this.formValues = {
        "first_name": "Tony",
        "last_name": "Stark"
    }

})

Now let’s look at one of the test cases for the onAdd() method.

  • Pretty much every test can be broken down to the following parts
    • Setup of test data and the necessary resources
    • Mock/Spy the components that the test case touches that are outside the scope of the “Unit of code” that is being tested
    • Invoke the “Unit Of code” that is to be tested (A method in this case)
    • Check if the behavior is as expected
  • In this case, we need to inspect if the form values are being inserted in to the store after validation and if the store is synced with the backend service. So we spy on both of these methods and test if they are invoked appropriately.
it('onAdd should add valid form values to the store', function(){

    //Since we are going to test the feature to add a new Super Hero, let's setup the sample input data

    const form = this.cmp.lookupReference('hero_add_form')
    const store = this.cmp.getViewModel().getStore('heroStore')
    form.getForm().setValues(this.formValues)
    const model = Ext.create('SuperHeroes.model.Hero', this.formValues);

    spyOn(store,"add")
    spyOn(store,"sync")
    spyOn(this.ctrl, "generateModel").and.returnValue(model)


    this.ctrl.onAdd()

    expect(store.sync).toHaveBeenCalled()
    expect(store.add).toHaveBeenCalledWith(model)
})

Ensure that there are sufficient test cases for each unit of code and all the necessary behaviors have their own separate tests. All critical behaviors of each unit of code must be covered by these tests (As an Example: Every branch condition of an if/else statement must have it’s own test case if the branching imparts a  different  behavior to the unit of code being tested).

Running the tests

Run the tests using karma by invoking “karma start test/karma.conf.js” from the root of the project.

user@userbox ~/dev/Unit-Testing-Sencha-ExtJS $ karma start test/karma.conf.js
13 09 2017 11:23:59.998:WARN [watcher]: Pattern "/home/uhsarp/dev/Unit-Testing-Sencha-ExtJS/test/spec/lib/**/*.ts" does not match any file.
13 09 2017 11:24:00.415:INFO [compiler.karma-typescript]: Compiling project using Typescript 2.5.2
...
13 09 2017 11:24:01.428:INFO [compiler.karma-typescript]: Compiled 2 files in 969 ms.
13 09 2017 11:24:02.075:WARN [karma]: No captured browser, open http://localhost:9876/
13 09 2017 11:24:02.100:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
13 09 2017 11:24:02.101:INFO [launcher]: Launching browser Chrome with unlimited concurrency
13 09 2017 11:24:02.364:INFO [launcher]: Starting browser Chrome
13 09 2017 11:24:09.504:INFO [Chrome 60.0.3112 (Linux 0.0.0)]: Connected on socket XYc5FNcTh984RlZ_AAAA with id 71514996
Chrome 60.0.3112 (Linux 0.0.0): Executed 8 of 9 SUCCESS (0 secs / 0.104 secs)
13 09 2017 11:24:12.658:WARN [web-server]: 404: /get?_dc=1505316252655&page=1&start=0&limit=25
WARN: '[W] [Ext.define] Duplicate class name 'SuperHeroes.view.HeroGridController' specified, must be a non-empty string'
WARN: '[W] [Ext.define] Duplicate class name 'SuperHeroes.view.HeroGridModel' specified, must be a non-empty string'
Chrome 60.0.3112 (Linux 0.0.0): Executed 9 of 9 SUCCESS (0.129 secs / 0.129 secs)

Source Code
https://github.com/batchu/Unit-Testing-Sencha-ExtJS.git

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.