Tuesday, November 19, 2013

Getting Started with Karma for Polymer Testing


The end of Patterns in Polymer will deal with testing and maintainability. I can't leave research on testing until the book is nearly done. In fact, it is rather important that I nail it down very early so that I have assurance that the code referenced throughout the book works - especially as the Polymer project continues to evolve.

I find it interesting that my posts on testing tend to have fewer readers. I suppose I understand in a way why this is. It is always fun to play and create with new tools. I like it too. But a tool that does not support strong testing practices is a tool that promotes codebases that will rot. So I quickly move from introduction to testing. If I am not satisfied, it is time to drop the new shininess for something better.

Testing is one of the reasons I love Dart so much. I have already written some extensive tests in Polymer.dart, so I am quite comfortable with that aspect of testing in Patterns in Polymer. But testing the JavaScript version? No idea.

The only mention of testing on the project page discusses building Polymer itself, not a project. Searches currently return very little, so it seems that I am on my own for now. In JavaScript land, I have been defaulting to Karma and Jasmine.

I already have Karma installed on my system:
➜  bootstrap-panels git:(master) karma --version
Karma version: 0.10.2
If I did not, installing it is a simple matter of installing Node.js and then npm install -g karma.

There is a lot to love about Karma, but getting started may at the top of the list. The built-in karma init is wonderful:
➜  bootstrap-panels git:(master) karma init

Which testing framework do you want to use ?
Press tab to list possible options. Enter to move to the next question.
> jasmine

Do you want to use Require.js ?
This will add Require.js plugin.
Press tab to list possible options. Enter to move to the next question.
> no

Do you want to capture a browser automatically ?
Press tab to list possible options. Enter empty string to move to the next question.
> Chrome
> 

What is the location of your source and test files ?
You can use glob patterns, eg. "js/*.js" or "test/**/*Spec.js".
Enter empty string to move to the next question.
> test/**/*Spec.js
> 

Should any of the files included by the previous patterns be excluded ?
You can use glob patterns, eg. "**/*.swp".
Enter empty string to move to the next question.
> 

Do you want Karma to watch all the files and run the tests on change ?
Press tab to list possible options.
> yes

Config file generated at "/home/chris/repos/polymer-book/play/bootstrap-panels/karma.conf.js".
Normally I would also include PhantomJS in the list of browsers for better continuous integration testing support, but tonight I just want to smoke test my testing. Chrome should be sufficient for that.

I need two changes to the default values that I wrote to karma.conf.js. First, I need to include the Polymer JavaScript source into the Jasmine context.html page. This is done by adding 'scripts/**/*.js' to the list of files section the configuration. The other change that I make is to load fixture data. I am not a huge fan of fixture files for JavaScript testing, but I am even less a fan of string manipulation in JavaScript, so I use fixtures. For these, I need to include them in the list of files and add an html2js preprocessor:
module.exports = function(config) {
  config.set({
    // ...
    preprocessors: {
      'test/*.html': 'html2js'
    },

    // list of files / patterns to load in the browser
    files: [
      'scripts/**/*.js',
      'test/**/*Spec.js',
      'test/*.html'
    ],
    // ...
  });
};
For my tests tonight, I create pricing-plan-test.html fixture file with minimal content:
<pricing-plan>pricing plan 01</pricing-plan>
With that, I am ready to write my first test in test/PricingPlanSpec.js. I start by loading that fixture in a beforeEach() block:
describe('<pricing-plan>', function(){
  beforeEach(function(){
    document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
  });
  // ...
});
Since my current fixture is just testing default values (there are no attributes), I create a "defaults" testing group. Before each test in this group, I find my fixture element:
describe('<pricing-plan>', function(){
  beforeEach(function(){
    document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
  });

  describe('defaults', function(){
    beforeEach(function(){
      var it = document.getElementsByTagName('pricing-plan')[0];
    });
    // tests here...
  });
});
As for the test itself, I just start tonight by verifying that the element is there:
describe('<pricing-plan>', function(){
  beforeEach(function(){
    document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
  });

  describe('defaults', function(){
    beforeEach(function(){
      var it = document.getElementsByTagName('pricing-plan')[0];
    });

    it('should create an element', function() {
      expect(it).toNotBe(undefined);
    });
  });
});
With that, I can fire up my test to find:
➜  bootstrap-panels git:(master) ✗ karma start
INFO [karma]: Karma v0.10.2 server started at http://localhost:9876/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 33.0.1707 (Linux)]: Connected on socket MUJraHBb3vxX3N1lzlTY
Chrome 33.0.1707 (Linux): Executed 1 of 1 SUCCESS (0.309 secs / 0.018 secs)
That is not quite success, however. Polymer takes some time to load and my test is not waiting for that before running the tests. I am only verifying that the bare tag exists, not that the Polymer element is doing anything. I think that is going to be a more significant undertaking, possible involving requirejs to ensure script load order (Polymer really needs to come first). I will pick back up with that tomorrow.

UPDATE: I was able to get the Polymer code firing with some addition beforeEach() setup:
describe('<pricing-plan>', function(){
  beforeEach(function(){
    var script = document.createElement("script");
    script.src = "/base/scripts/polymer.min.js";
    document.getElementsByTagName("head")[0].appendChild(script);

    var link = document.createElement("link");
    link.rel = "import";
    link.href = "/base/pricing-plan.html";
    document.getElementsByTagName("head")[0].appendChild(link);

    document.body.innerHTML = __html__['test/pricing-plan-fixture.html'];
  });
  // tests here...
});
By manually appending Polymer and my polymer element to the script tag, I was able to get the actual Polymer element content to show up in the Karma test runner:



I had to instruct Karma to serve, but not automatically include, these files:
    // ...
    // list of files / patterns to load in the browser
    files: [
      {pattern: '*.html', included: false},
      {pattern: 'scripts/**/*.js', included: false},
      'test/**/*Spec.js',
      'test/*.html'
    ],
    // ...
I am not 100% sold that this is better than futzing with require.js, but it seems to work. I will play more with this tomorrow.


Day #940

3 comments:

  1. Hello Chris,
    This is a very useful article!
    I tried this approach and I saw that I need to listen to 'polymer-ready' event to make sure I can start interacting with the polymer element's DOM.

    I am trying to figure out the best way to setup multiple test suites for different polymer elements.Though I make sure polymer is not loaded again for different suites, I no longer can depend on 'polymer-ready' to ensure my element is loaded since it's only triggered the first time when the first suite's fixture is loaded.

    I am considering using iFrame for each of the suites,but that doesn't seem to be a good practice.

    Could you share your thoughts on how you went about it?

    Thanks!
    Vj.

    ReplyDelete
  2. Hi Vj.

    It seems 'polymer-ready' event doesn't work, don't know why. But I am now using 'WebComponentsReady' event instead (I'm using Polymer 1.0).

    What I did is creating one javascript file (says `config-test.js`) that I will include it first before any test spec. In this file, I have javascript code that will inject my elements being tested using <link rel="import"> tag and wait for 'WebComponentsReady' event before continue.

    It seems karma/jasmine will run my test in in order synchronizly so it is ensured that Polymer will be ready before any other tests will run.

    Here is my `config-test.js`

    ```
    describe('Setup', function () {
    beforeAll(function (done) {
    var importUrls = [
    'base/elements/my-element-1.html',
    'base/elements/my-element-2.html'
    ];
    var headEl = document.getElementsByTagName('head')[0];
    importUrls.forEach(function (importUrl) {
    var linkEl = document.createElement('link');
    linkEl.rel = 'import';
    linkEl.href = importUrl;
    headEl.appendChild(linkEl);
    });
    document.addEventListener('WebComponentsReady', function (e) {
    done();
    });
    });

    it('Dummy', function () {
    });
    });
    ```

    And this is `files` array config in my `karma.config.js`
    ```
    // list of files / patterns to load in the browser
    files: [
    'config-test.js',
    { pattern: 'my-element-1.html', watched: true, included: false, served: true, nocache: true },
    { pattern: 'my-element-2.html', watched: true, included: false, served: true, nocache: true }
    ],
    ```

    Regards,
    Art

    ReplyDelete
  3. Thanks for sharing, Chris!
    I'm also diving into this same Web Components testing world.

    One thing I found is that recent `karma` version supports including html file as `<link rel="import">` tag. (see https://github.com/karma-runner/karma/issues/828)

    However, I found some strange behavior regarding this feature and just filed the issue for it here https://github.com/karma-runner/karma/issues/1563

    Regards,
    Art

    ReplyDelete