Testing Dojo application (Detailed)

Testing Services

Intern support BrowserStack , SauceLabs and TestingBot run tests remotely on other services. You can use one of these services, register for an account and credentials to cli-test-intern. By default, all testing services will run tests on IE11, Firefox and Chrome and other browsers.

BrowserStack

Use BrowserStack service, the need to provide access key and user name. Access key and user name can be specified on the command line or as an environment variable, see Intern documents .

dojo test -a -c browserstack -k <accesskey> --userName <username>

Or use environment variables

BROWSERSTACK_USERNAME=<username> BROWSERSTACK_ACCESS_KEY=<key> dojo test -a -c browserstack

SauceLabs

Use SauceLabs service, the need to provide access key and user name. Access key and user name can be specified on the command line or as an environment variable, see Intern documents .

dojo test -a -c saucelabs -k <accesskey> --userName <username>

Or use environment variables

SAUCE_USERNAME=<username> SAUCE_ACCESS_KEY=<key> dojo test -a -c saucelabs

TestingBot

Use TestingBot service, the need for key and secret. Key and secret can be specified on the command line or as an environment variable, see Intern documents .

dojo test -a -c testingbot -k <key> -s <secret>

Or use environment variables

TESTINGBOT_SECRET=<secret> TESTINGBOT_KEY=<key> dojo test -a -c saucelabs

harness

When you are using @dojo/framework/testing, the harness()most important API, mainly used to set each test and provide context for the implementation of a virtual DOM assertions and interaction. When the object is updated propertiesor children, when component failure and, mirroring the behavior of the core member, and does not require any special or custom logic.

API

interface HarnessOptions {
    customComparators?: CustomComparator[];
    middleware?: [MiddlewareResultFactory<any, any, any>, MiddlewareResultFactory<any, any, any>][];
}

harness(renderFunction: () => WNode, customComparators?: CustomComparator[]): Harness;
harness(renderFunction: () => WNode, options?: HarnessOptions): Harness;
  • renderFunction: Function that returns the UUT WNode
  • customComparators: Custom a group descriptor. Each descriptor is a function of the comparator, for comparing selectorand propertytargeting ofproperties
  • options: Harness expansion options, including customComparatorsand a set of middleware / mocks tuples.

harness function returns an Harnessobject that provides several API to interact with the unit under test:

Harness

  • expect: Perform a complete rendering results assertion of the unit under test
  • expectPartial: Performing partial rendering results assertion UUT
  • trigger: Used to trigger functions on the device under test node
  • getRender: The index provided by the corresponding renderer returns from the harness

Using @dojo/framework/corethe w()function generating means for testing is a very simple:

const { describe, it } = intern.getInterface('bdd');
import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import harness from '@dojo/framework/testing/harness';
import { w, v } from '@dojo/framework/widget-core/d';

class MyWidget extends WidgetBase<{ foo: string }> {
    protected render() {
        const { foo } = this.properties;
        return v('div', { foo }, this.children);
    }
}

const h = harness(() => w(MyWidget, { foo: 'bar' }, ['child']));

Shown, harness supports the following functions tsx. README document remaining examples use the programmatic w()API, in unit testing can see more tsxexamples.

const h = harness(() => <MyWidget foo="bar">child</MyWidget>);

renderFunctionDelay is performed, it is possible to include additional logic to assert the operation member between propertiesand children.

describe('MyWidget', () => {
  it('renders with foo correctly', () => {
        let foo = 'bar';

        const h = harness(() => {
            return w(MyWidget, { foo }, [ 'child' ]));
        };

        h.expect(/** assertion that includes bar **/);
        // update the property that is passed to the widget
        foo = 'foo';
        h.expect(/** assertion that includes foo **/)
  });
});

Mocking Middleware

When initializing harness, mock intermediate can be specified as HarnessOptionspart of the value. Mock Middleware is defined as the tuple of the original and implementations mock middleware middleware thereof. Mock the same way to create middleware and other middleware.

import myMiddleware from './myMiddleware';
import myMockMiddleware from './myMockMiddleware';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
    it('renders', () => {
        const h = harness(() => <MyWidget />, { middleware: [[myMiddleware, myMockMiddleware]] });
        h.expect(/** assertion that executes the mock middleware instead of the normal middleware **/);
    });
});

Harness automatically mock many of the core middleware and injected into any of their middleware needs:

  • invalidator
  • setProperty
  • destroy

Dojo Mock Middleware

When tested using the Dojo member middleware, the middleware may be used there are many mock. Mock will export a factory, the factory will create a limited scope mock middleware will be used in each test.

Mock nodeMiddleware

Use @dojo/framework/testing/mocks/middleware/nodeof the createNodeMockcan a mock intermediate node. To set the desired value returned from the node mock, call the mock node middleware created and incoming keyand desired DOM node.

import createNodeMock from '@dojo/framework/testing/mocks/middleware/node';

// create the mock node middleware
const mockNode = createNodeMock();

// create a mock DOM node
const domNode = {};

// call the mock middleware with a key and the DOM
// to return.
mockNode('key', domNode);
Mock intersectionMiddleware

Use @dojo/framework/testing/mocks/middleware/intersectionof the createIntersectionMockcan a mock intersection middleware. To set the desired value returned from the intersection mock, call the mock created intersection middleware, and pass keyintersection details and expectations.

Consider the following components:

import { create, tsx } from '@dojo/framework/core/vdom';
import intersection from '@dojo/framework/core/middleware/intersection';

const factory = create({ intersection });

const App = factory(({ middleware: { intersection } }) => {
    const details = intersection.get('root');
    return <div key="root">{JSON.stringify(details)}</div>;
});

Use mock intersection middleware:

import { tsx } from '@dojo/framework/core/vdom';
import createIntersectionMock from '@dojo/framework/testing/mocks/middleware/intersection';
import intersection from '@dojo/framework/core/middleware/intersection';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
    it('test', () => {
        // create the intersection mock
        const intersectionMock = createIntersectionMock();
        // pass the intersection mock to the harness so it knows to
        // replace the original middleware
        const h = harness(() => <App key="app" />, { middleware: [[intersection, intersectionMock]] });

        // call harness.expect as usual, asserting the default response
        h.expect(() => <div key="root">{`{"intersectionRatio":0,"isIntersecting":false}`}</div>);

        // use the intersection mock to set the expected return
        // of the intersection middleware by key
        intersectionMock('root', { isIntersecting: true });

        // assert again with the updated expectation
        h.expect(() => <div key="root">{`{"isIntersecting": true }`}</div>);
    });
});
Mock resizeMiddleware

Use @dojo/framework/testing/mocks/middleware/resizeof the createResizeMockcan resize a mock middleware. To set the desired value returned from the resize mock, call the mock resize middleware to create and pass keya rectangular area to accommodate the content and expectations.

const mockResize = createResizeMock();
mockResize('key', { width: 100 });

Consider the following components:

import { create, tsx } from '@dojo/framework/core/vdom'
import resize from '@dojo/framework/core/middleware/resize'

const factory = create({ resize });

export const MyWidget = factory(function MyWidget({ middleware }) => {
    const  { resize } = middleware;
    const contentRects = resize.get('root');
    return <div key="root">{JSON.stringify(contentRects)}</div>;
});

Use mock resize middleware:

import { tsx } from '@dojo/framework/core/vdom';
import createResizeMock from '@dojo/framework/testing/mocks/middleware/resize';
import resize from '@dojo/framework/core/middleware/resize';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
    it('test', () => {
        // create the resize mock
        const resizeMock = createResizeMock();
        // pass the resize mock to the harness so it knows to replace the original
        // middleware
        const h = harness(() => <App key="app" />, { middleware: [[resize, resizeMock]] });

        // call harness.expect as usual
        h.expect(() => <div key="root">null</div>);

        // use the resize mock to set the expected return of the resize middleware
        // by key
        resizeMock('root', { width: 100 });

        // assert again with the updated expectation
        h.expect(() => <div key="root">{`{"width":100}`}</div>);
    });
});
Mock StoreMiddleware

Use @dojo/framework/testing/mocks/middleware/storeof createMockStoreMiddlewarecan store a strongly typed mock middleware also supports mock process. To store a mock of the process, a tuple can be passed from the original and store Process stub process thereof. Middleware will instead call stub, instead of calling the original process. If you do not pass stub, the middleware will stop calling all of the process.

To modify the value mock store, you need to call mockStoreand pass a set of store operations function returns. This will store the injection pathfunction, to create a point to modify the state of a pointer.

mockStore((path) => [replace(path('details', { id: 'id' })]);

Consider the following components:

src/MyWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom'
import { myProcess } from './processes';
import MyState from './interfaces';
// application store middleware typed with the state interface
// Example: `const store = createStoreMiddleware<MyState>();`
import store from './store';

const factory = create({ store }).properties<{ id: string }>();

export default factory(function MyWidget({ properties, middleware: store }) {
    const { id } = properties();
    const { path, get, executor } = store;
    const details = get(path('details');
    let isLoading = get(path('isLoading'));

    if ((!details || details.id !== id) && !isLoading) {
        executor(myProcess)({ id });
        isLoading = true;
    }

    if (isLoading) {
        return <Loading />;
    }

    return <ShowDetails {...details} />;
});

Use mock store Middleware:

tests/unit/MyWidget.tsx

import { tsx } from '@dojo/framework/core/vdom'
import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store';
import harness from '@dojo/framework/testing/harness';

import { myProcess } from './processes';
import MyWidget from './MyWidget';
import MyState from './interfaces';
import store from './store';

// import a stub/mock lib, doesn't have to be sinon
import { stub } from 'sinon';

describe('MyWidget', () => {
     it('test', () => {
          const properties = {
               id: 'id'
          };
         const myProcessStub = stub();
         // type safe mock store middleware
         // pass through an array of tuples `[originalProcess, stub]` for mocked processes
         // calls to processes not stubbed/mocked get ignored
         const mockStore = createMockStoreMiddleware<MyState>([[myProcess, myProcessStub]]);
         const h = harness(() => <MyWidget {...properties} />, {
             middleware: [store, mockStore]
         });
         h.expect(/* assertion template for `Loading`*/);

         // assert again the stubbed process
         expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy();

         mockStore((path) => [replace(path('isLoading', true)]);
         h.expect(/* assertion template for `Loading`*/);
         expect(myProcessStub.calledOnce()).toBeTruthy();

         // use the mock store to apply operations to the store
         mockStore((path) => [replace(path('details', { id: 'id' })]);
         mockStore((path) => [replace(path('isLoading', true)]);

         h.expect(/* assertion template for `ShowDetails`*/);

         properties.id = 'other';
         h.expect(/* assertion template for `Loading`*/);
         expect(myProcessStub.calledTwice()).toBeTruthy();
         expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy();
         mockStore((path) => [replace(path('details', { id: 'other' })]);
         h.expect(/* assertion template for `ShowDetails`*/);
     });
});

Custom Comparators

In some cases, we do not know the exact value of the property during the test, so it is necessary to use a custom comparison descriptors (custom compare descriptor).

Descriptor has a virtual node location to be examined for selectora custom application and a comparison of attribute names, and receives the actual value returns a boolean predicate result of the comparator function.

const compareId = {
    selector: '*', // all nodes
    property: 'id',
    comparator: (value: any) => typeof value === 'string' // checks the property value is a string
};

const h = harness(() => w(MyWidget, {}), [compareId]);

For all the assertions returned harnessAPI will only idproperty use comparatorfor testing, rather than the standard equality test.

selectors

harnessAPI supports CSS style selectors concept, to assert to locate and operate a virtual nodes in the DOM. View support for a complete list of selectors for more information.

In addition to the standard API also provides:

  • Support the positioning node keyattributes abbreviated @notation
  • When using the standard .time to locate the pattern classes, using classesproperties instead classProperties

harness.expect

Testing is the most common requirement is part of the assertion renderoutput structure of the function. expectReceiving a return result of rendering a desired component under test as a function of the parameters.

API

expect(expectedRenderFunction: () => DNode | DNode[], actualRenderFunction?: () => DNode | DNode[]);
  • expectedRenderFunction: Returning node desired DNodefunction structure
  • actualRenderFunction: An optional function, returns the actual asserted DNodestructure
h.expect(() =>
    v('div', { key: 'foo' }, [w(Widget, { key: 'child-widget' }), 'text node', v('span', { classes: ['class'] })])
);

expect You may also receive a second optional parameter and returns the result to the rendering function of the assertion.

h.expect(() => v('div', { key: 'foo' }), () => v('div', { key: 'foo' }));

If the actual rendering of the rendered output and a desired output different throw an exception, and visualized using a structured approach, using (A)(actual value) and the (E)(expected value) indicates that all the different points.

Example assertion failure output:

v('div', {
    'classes': [
        'root',
(A)     'other'
(E)     'another'
    ],
    'onclick': 'function'
}, [
    v('span', {
        'classes': 'span',
        'id': 'random-id',
        'key': 'label',
        'onclick': 'function',
        'style': 'width: 100px'
    }, [
        'hello 0'
    ])
    w(ChildWidget, {
        'id': 'random-id',
        'key': 'widget'
    })
    w('registry-item', {
        'id': true,
        'key': 'registry'
    })
])

harness.trigger

harness.trigger()In selectorcalling on the node location namespecified function.

interface FunctionalSelector {
    (node: VNode | WNode): undefined | Function;
}

trigger(selector: string, functionSelector: string | FunctionalSelector, ...args: any[]): any;
  • selector: Used to find the target node selector
  • functionSelector: Either the name of the function is called to find the properties from the node, or a function that returns a function selector from the attribute node
  • args: In order to locate the parameters passed to the function

If you have a return result, return is the result of the function is triggered.

Example usage:

// calls the `onclick` function on the first node with a key of `foo`
h.trigger('@foo', 'onclick');
// calls the `customFunction` function on the first node with a key of `bar` with an argument of `100`
// and receives the result of the triggered function
const result = h.trigger('@bar', 'customFunction', 100);

functionalSelectorMember function returns properties. Function will be triggered, and the use of ordinary strings functionSelectorway of the same.

Example usage:

Assuming the following VDOM tree structure:

v(Toolbar, {
    key: 'toolbar',
    buttons: [
        {
            icon: 'save',
            onClick: () => this._onSave()
        },
        {
            icon: 'cancel',
            onClick: () => this._onCancel()
        }
    ]
});

And you want to trigger the save button onClickfunction.

h.trigger('@buttons', (renderResult: DNode<Toolbar>) => {
    return renderResult.properties.buttons[0].onClick;
});

Note: If you can not find the specified selector, then triggerthrows an error.

harness.getRender

harness.getRender() Returns the index of the specified renderer, if no index of the last renderer is returned.

getRender(index?: number);
  • index: To return to the renderer index

Example usage:

// Returns the result of the last render
const render = h.getRender();
// Returns the result of the render for the index provided
h.getRender(1);

Assertion Templates

Assertion template (assertion template) allows you to build the desired rendering functions for incoming h.expect()in. The idea behind the assertion template from the often asserted that the entire render output, and the need to revise certain parts of the assertion.

To assert template, you need to import the module:

import assertionTemplate from '@dojo/framework/testing/assertionTemplate';

Then, in your tests, you can write a basic assertion, it is the default rendering state components:

Consider the following components:

src/widgets/Profile.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { v } from '@dojo/framework/widget-core/d';

import * as css from './styles/Profile.m.css';

export interface ProfileProperties {
    username?: string;
}

export default class Profile extends WidgetBase<ProfileProperties> {
    protected render() {
        const { username } = this.properties;
        return v('h1', { classes: [css.root] }, [`Welcome ${username || 'Stranger'}!`]);
    }
}

The basic assertion follows:

tests/unit/widgets/Profile.ts

const { describe, it } = intern.getInterface('bdd');
import harness from '@dojo/framework/testing/harness';
import assertionTemplate from '@dojo/framework/testing/assertionTemplate';
import { w, v } from '@dojo/framework/widget-core/d';

import Profile from '../../../src/widgets/Profile';
import * as css from '../../../src/widgets/styles/Profile.m.css';

const profileAssertion = assertionTemplate(() =>
    v('h1', { classes: [css.root], '~key': 'welcome' }, ['Welcome Stranger!'])
);

Write in the test:

tests/unit/widgets/Profile.ts

const profileAssertion = assertionTemplate(() =>
    v('h1', { classes: [css.root], '~key': 'welcome' }, ['Welcome Stranger!'])
);

describe('Profile', () => {
    it('default renders correctly', () => {
        const h = harness(() => w(Profile, {}));
        h.expect(profileAssertion);
    });
});
it('default renders correctly', () => {
    const h = harness(() => w(Profile, {}));
    h.expect(profileAssertion);
});

Now we look for the Profileparts passed usernameafter the property, how to test the output:

tests/unit/widgets/Profile.ts

describe('Profile', () => {
    ...

  it('renders given username correctly', () => {
    // update the expected result with a given username
    const namedAssertion = profileAssertion.setChildren('~welcome', [
      'Welcome Kel Varnsen!'
    ]);
    const h = harness(() => w(Profile, { username: 'Kel Varnsen' }));
    h.expect(namedAssertion);
  });
});

Here, we use the baseAssertion setChildren()api, and then we use a special ~selector to locate the key value ~messageof the node. ~keyProperty (tsx the template is assertion-key) is a special attribute assertion template will be deleted when asserted, thus rendering the structure when the match will not be displayed. This feature allows you to modify the assertion template to enable simple selection nodes without the need to expand the actual components render functions. Once we get to the messagenode, we can be set to a desired sub-node the number 5and then h.expectuse the template generates. It should be noted that the assertion templates when setting up a new value always returns assertion template, which can ensure that you do not accidentally modify an existing template (may lead to other tests fail), and allows you based on the new template, incremental step by step Construction of a new template.

Assertion template has the following API:

insertBefore(selector: string, children: () => DNode[]): AssertionTemplateResult;
insertAfter(selector: string, children: () => DNode[]): AssertionTemplateResult;
insertSiblings(selector: string, children: () => DNode[], type?: 'before' | 'after'): AssertionTemplateResult;
append(selector: string, children: () => DNode[]): AssertionTemplateResult;
prepend(selector: string, children: () => DNode[]): AssertionTemplateResult;
replaceChildren(selector: string, children: () => DNode[]): AssertionTemplateResult;
setChildren(selector: string, children: () => DNode[], type?: 'prepend' | 'replace' | 'append'): AssertionTemplateResult;
setProperty(selector: string, property: string, value: any): AssertionTemplateResult;
setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult;
getChildren(selector: string): DNode[];
getProperty(selector: string, property: string): any;
getProperties(selector: string): any;
replace(selector: string, node: DNode): AssertionTemplateResult;
remove(selector: string): AssertionTemplateResult;

Mocking

You may have noticed that when testing components, we mainly test the properties that various modifications, the user interface is correct rendering. They do not contain real business logic, but you may want to test whether, for example, click the button calls the property method. This test method does not actually care about what has been done, only care about the correct call interface. In this case, you can use something like Sinon library.

src/widgets/Action.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { v, w } from '@dojo/framework/widget-core/d';
import Button from '@dojo/widgets/button';

import * as css from './styles/Action.m.css';

export default class Action extends WidgetBase<{ fetchItems: () => void }> {
    protected render() {
        return v('div', { classes: [css.root] }, [w(Button, { onClick: this.handleClick, key: 'button' }, ['Fetch'])]);
    }
    private handleClick() {
        this.properties.fetchItems();
    }
}

You may want to test, when you click the button whether it will call this.properties.fetchItemsthe method.

tests/unit/widgets/Action.ts

const { describe, it } = intern.getInterface('bdd');
import harness from '@dojo/framework/testing/harness';
import { w, v } from '@dojo/framework/widget-core/d';

import { stub } from 'sinon';

describe('Action', () => {
    const fetchItems = stub();
    it('can fetch data on button click', () => {
        const h = harness(() => w(Home, { fetchItems }));
        h.expect(() => v('div', { classes: [css.root] }, [w(Button, { onClick: () => {}, key: 'button' }, ['Fetch'])]));
        h.trigger('@button', 'onClick');
        assert.isTrue(fetchItems.calledOnce);
    });
});

In this case, you can mock an Action member fetchItems, the method attempts to obtain the data item. Then you can use @buttonlocate button and the trigger button onClickevent, and then check fetchItemswhether the method is called once.

For more mocking information, please read the Sinon document.

function test

And process load and execute the code unit testing different functional testing to load a page and test the application of interactive features in the browser.

If you want to check the contents of a page corresponding to the route, you can simplify the test by updating the links.

src/widgets/Menu.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { w } from '@dojo/framework/widget-core/d';
import Link from '@dojo/framework/routing/ActiveLink';
import Toolbar from '@dojo/widgets/toolbar';

import * as css from './styles/Menu.m.css';

export default class Menu extends WidgetBase {
    protected render() {
        return w(Toolbar, { heading: 'My Dojo App!', collapseWidth: 600 }, [
            w(
                Link,
                {
                    id: 'home', // add id attribute
                    to: 'home',
                    classes: [css.link],
                    activeClasses: [css.selected]
                },
                ['Home']
            ),
            w(
                Link,
                {
                    id: 'about', // add id attribute
                    to: 'about',
                    classes: [css.link],
                    activeClasses: [css.selected]
                },
                ['About']
            ),
            w(
                Link,
                {
                    id: 'profile', // add id attribute
                    to: 'profile',
                    classes: [css.link],
                    activeClasses: [css.selected]
                },
                ['Profile']
            )
        ]);
    }
}

When using the application, you need to click the profilelink, and then navigate to the user welcome page. You can write a functional test to verify this behavior.

tests/functional/main.ts

const { describe, it } = intern.getInterface('bdd');
const { assert } = intern.getPlugin('chai');

describe('routing', () => {
    it('profile page correctly loads', ({ remote }) => {
        return (
            remote
                // loads the HTML file in local node server
                .get('../../output/dev/index.html')
                // find the id of the anchor tag
                .findById('profile')
                // click on the link
                .click()
                // end this action
                .end()
                // find the h1 tag
                .findByTagName('h1')
                // get the text in the h1 tag
                .getVisibleText()
                .then((text) => {
                    // verify the content of the h1 tag on the profile page
                    assert.equal(text, 'Welcome Dojo User!');
                })
        );
    });
});

When you run a functional test, Dojo provides a page to interact with remoteobjects. Because the page is loaded and interact with the page is an asynchronous operation, it must be returned in the test remoteobject.

Functional test can be performed on the command line.

Command Line

npm run test:functional

This will HTML page is loaded into the remote instance of Chrome on your computer to test interactive features.

Functional testing is very useful, it can ensure that your browser, your program code to work properly as expected.

You can read more about Intern functional test content.

Guess you like

Origin blog.51cto.com/14193089/2423424