Unit Testing - ProjectEvergreen/create-evergreen-app GitHub Wiki

Unit testing is an important part of developing any application. This document details configuration and setup for unit testing using Karma or web-component-tester .

⚠️ We could use your help!

If you're interested in some unit testing related tasks, please consider checking out some related issues which we would love your help with! 🙏

  • Adding unit testing to projects like the website or todo-app
  • More documentation and examples of testing, like for event handling (Custom Events)

Karma (Recommended)

Setup

Karma is a bit configuration heavy, but very robust and provides great developer workflows like file watching / reload, persisting the test runner to enable test-driven development (TDD), and code coverage reports.

For this example, we'll be using webpack, Jasmine, and Puppeteer (headless chrome). Using webpack is not required (see later comments).

Install Karma and needed dependencies 📦

npm install istanbul-instrumenter-loader@^3.0.1 jasmine@^3.2.0 jasmine-core@^3.2.1 karma@^3.0.0 karma-chrome-launcher@^2.2.0 karma-coverage-istanbul-reporter@^2.0.4 karma-jasmine@^1.1.2 karma-junit-reporter@^1.2.0 karma-sourcemap-loader@^0.3.7 karma-webpack@^3.0.5
puppeteer@^0.11.0 --save-dev

Configure Karma ️️⚙️

There are two files for setting up Karma, both should be placed in the root of your project's repository.

Karma Test Shim

Used to bootstrap Karma CLI itself; karma-test-shim.js.

// Prevent Karma from running prematurely.
__karma__.loaded = function () {};

// Then we find all the tests.
const context = require.context('./src', true, /\.spec\.js$/);

// And load the modules.
context.keys().map(context);

// Finally, start Karma to run the tests.
__karma__.start();
Karma Configuration

Used to configure Karma with Jasmine, polyfills, unit testing / coverage options, etc.; karma.conf.js.

const path = require('path');
const webpackConfig = require('./webpack.config.common');
const isProductionBuild = process.env.NODE_ENV === 'production';
const shouldWatch = !isProductionBuild;
const shouldSingleRun = isProductionBuild;

process.env.CHROME_BIN = require('puppeteer').executablePath();
webpackConfig.devtool = 'inline-source-map';
webpackConfig.mode = 'development';

webpackConfig.module.rules.push({
  test: /\.js$/,
  enforce: 'post',
  exclude: [/\.spec.js$/, /node_modules/],
  loader: 'istanbul-instrumenter-loader',
  query: {
    esModules: true
  }
});

module.exports = function (config) {
  const logLevel = isProductionBuild ? config.LOG_DEBUG : config.LOG_INFO;

  config.set({
    basePath: '',
    files: [
      { pattern: './karma-test-shim.js', watched: false }
    ],
    preprocessors: {
      './karma-test-shim.js': ['webpack', 'sourcemap']
    },
    frameworks: ['jasmine'],
    plugins: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-junit-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('karma-webpack'),
      require('karma-sourcemap-loader')
    ],

    webpack: webpackConfig,

    reporters: ['progress', 'dots', 'junit', 'coverage-istanbul'],
    port: 9876,
    colors: true,
    logLevel: logLevel,
    autoWatch: shouldWatch,
    browsers: ['ChromiumHeadlessConfigured'],
    customLaunchers: {
      ChromiumHeadlessConfigured: {
        base: 'ChromeHeadless',
        flags: [
          '--no-sandbox',
          '--disable-setuid-sandbox'
        ]
      }
    },
    singleRun: shouldSingleRun,
    captureTimeout: 210000,
    browserDisconnectTolerance: 3,
    browserDisconnectTimeout: 210000,
    browserNoActivityTimeout: 210000,
    concurrency: Infinity,
    junitReporter: {
      outputDir: './reports/test-results/',
      outputFile: 'test-results.xml',
      suite: 'my-app',
      useBrowserName: false
    },
    coverageIstanbulReporter: {
      dir: path.join(__dirname, 'reports'),
      reports: ['html', 'cobertura', 'text-summary'],
      'report-config': {
        html: {
          subdir: 'test-coverage'
        },
        cobertura: {
          file: 'test-coverage/coverage.xml'
        },
        'text-summary': {}
      },
      fixWebpackSourcePaths: true,
      remapOptions: {
        exclude: [/\*.spec.ts/]
      },
      thresholds: {
        emitWarning: false,
        global: {
          statements: 90,
          branches: 80,
          functions: 90,
          lines: 90
        }
      }
    }
  });
};

If not using webpack, use the preprocessors API to process your application code as needed.

Create a test! 🔴 🔵

👉 Checkout our Technology Stack wiki docs to learn more about lit-html and LitElement

src/components/header/header.js - Our HeaderComponent

import { html, LitElement } from '@polymer/lit-element';

class HeaderComponent extends LitElement {

  render() {
    return html`      

      <header>

        <h1>Welcome to Create Evergreen App!</h1>
      
      </header>
    `;
  }
}

customElements.define('x-header', HeaderComponent);

src/components/header/header.spec.js - This is our unit test file, as loaded from karma-test-shim.js.

import './header.js';

describe('Header Component', () => {
  let header;

  beforeEach(async () => {
    header = document.createElement('eve-header');

    document.body.appendChild(header);

    // this is what makes the magic happen ✨
    await header.updateComplete;
  });

  afterEach(() => {
    header.remove();
    header = null;
  });

  describe('Default Behavior', () => {
    
    it('should have a greeting', () => { 
      const greeting = header.shadowRoot.querySelectorAll('header h1')[0];

      expect(greeting.innerHTML).toBe('Welcome to Create Evergreen App!');
    });

  });

});

For a more complex test setup, see this repo.

Run the tests! 🏃

Create an npm script in package.json

"scripts": {
  ...
  "test": "karma start"
}

And use it to run karma from the command line

$ npm run test

Happy testing! 🎉

For a complete working example, check out create-evergreen-app.

Other Browsers

For convenience, Create Evergreen App comes with the dependencies needed to run two browsers out of the box

  1. Chrome (headless w/Puppeteer)
  2. Firefox

Firefox

Using Firefox

  1. Firefox (and other browsers) will likely need Custom Elements and Shadow DOM polyfilled. To add this polyfill in Karma, uncomment the lines in the files array in karma.conf.js related to ~@webcomponents.
  2. Add Firefox to the browsers array in karma.conf.js.

Chrome headless is enabled by default since it is the most portable between local and continuous integration environments.

Web Component Tester

Web Component Tester was developed by the Polymer team with the goal of providing a test driven runtime and environment. In addition to being a test runner, it also provides Mocha and Chai out of the box with support for both TDD and BDD testing flavors.

Setup ⚙️

There are a couple steps needed to get started with unit testing.

For the purposes of this guide, we will be setting up Chrome to run in headless mode. This makes it easy to run in continuous integration environments.

Java

WCT use Selenium under the hood, so you will need to make sure that you have a JRE installed and available on your PATH

$ which java
/usr/bin/java
$ java --version
java 10.0.1 2018-04-17
Java(TM) SE Runtime Environment 18.3 (build 10.0.1+10)
Java HotSpot(TM) 64-Bit Server VM 18.3 (build 10.0.1+10, mixed mode)

If you use Docker, checkout this handy container that has all the essential NodeJS development tools ready out of the box, including Java and Puppeteer!

Install WCT and needed dependencies 📦

# yarn
$ yarn add wct-browser-legacy wct-headless web-component-tester @polymer/test-fixture @webcomponents/webcomponentsjs --dev

# or npm
$ npm install wct-browser-legacy wct-headless web-component-tester @polymer/test-fixture @webcomponents/webcomponentsjs --save-dev

Configure WCT ⚙️

Add this to the root of your project in a file called wct.conf.json.

{
  "plugins": {
    "local": {
      "disabled": true
    },
    "headless": {
      "browsers": [
        "chrome"
      ],
      "browsersOptions": {
        "chrome": [
          "window-size=1920,1080",
          "headless",
          "disable-gpu",
          "no-sandbox"
        ]
      }
    }
  }
}

Create a test! 🔴 🔵

👉 Checkout our Technology Stack wiki docs to learn more about lit-html and LitElement

src/greeting.js - Our GreetingComponent

import { LitElement, html } from '@polymer/lit-element';

class GreetingComponent extends LitElement {
  
  static get properties() {
    return {
      name: {
        type: String,
        attrName: 'name'
      }
    };
  }

  _render(props) {
    return html`
      <style>
        :host .name {
          color: green;
        }
      </style>
      
      <h1 class="greeting">Hello <span class="name">${props.name}</span>!</h1>
    `;
  }
}

customElements.define('x-greeting', GreetingComponent);

test/index.html - Our test scaffold that pulls in polyfills and sets up our component in a fixture for unit testing.

<!DOCTYPE html>
<html>

  <head>
    <meta charset="utf-8">
    <script src="../node_modules/wct-browser-legacy/browser.js"></script>
    <script src="../node_modules/@polymer/test-fixture/test-fixture-mocha.js"></script>
    <script src="../node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>
    <script type="module" src="../src/greeting.js"></script>
    
    <test-fixture id="greeting">
      <template>
        <x-greeting name="Owen"></x-greeting>
      </template>
    </test-fixture>
      
    <script>
      WCT.loadSuites([
       './greeting.spec.js'
      ]);
    </script>

  </head>
  
</html>

test/greeting.spec.js - This is our unit test file. It will be loaded by our scaffold and will run tests for us.

describe('GreetingComponent', () => {
  const name = 'Owen';
  let componentRoot;
  let greetingContainer;
  let nameContainer;

  beforeEach(() => {
    componentRoot = fixture('greeting').shadowRoot;

    greetingContainer = componentRoot.querySelectorAll('.greeting');
    nameContainer = componentRoot.querySelectorAll('.name');
  });

  it('should have a container element for the greeting', () => {
    expect(greetingContainer.length).to.equal(1);
  });

  it('should have a container element for the name', () => {
    expect(nameContainer.length).to.equal(1);
  });

  it('should display the full greeting in the greeting container element', () => {
    expect(greetingContainer[0].textContent).to.equal(`Hello ${name}!`);
  });

  it('should display just the name attribute in name container element', () => {
    expect(nameContainer[0].textContent).to.equal(name);
  });
});

Run the tests! 🏃

Create an npm script in package.json

"scripts": {
  ...
  "test": "wct --npm --expanded"
}

And use it to run wct from the command line

$ yarn test

Happy testing! 🎉

For a complete working example, check out component-simple-slider.

⚠️ **GitHub.com Fallback** ⚠️